Some fixes and additions

This commit is contained in:
Torsten Schulz
2025-07-09 14:28:35 +02:00
parent 5029be81e9
commit fceea5b7fb
32 changed files with 4373 additions and 1294 deletions

View File

@@ -1,236 +1,291 @@
<template>
<div class="contenthidden">
<StatusBar ref="statusBar" />
<div class="contentscroll">
<h2>{{ $t('falukant.branch.title') }}</h2>
<BranchSelection :branches="branches" :selectedBranch="selectedBranch" @branchSelected="onBranchSelected"
@createBranch="createBranch" @upgradeBranch="upgradeBranch" ref="branchSelection" />
<DirectorInfo v-if="selectedBranch" :branchId="selectedBranch.id" ref="directorInfo" />
<SaleSection v-if="selectedBranch" :branchId="selectedBranch.id" ref="saleSection" />
<ProductionSection v-if="selectedBranch" :branchId="selectedBranch.id" :products="products"
ref="productionSection" />
<StorageSection v-if="selectedBranch" :branchId="selectedBranch.id" ref="storageSection" />
<RevenueSection v-if="selectedBranch" :products="products"
:calculateProductRevenue="calculateProductRevenue" :calculateProductProfit="calculateProductProfit"
ref="revenueSection" />
</div>
<StatusBar ref="statusBar" />
<div class="contentscroll">
<h2>{{ $t('falukant.branch.title') }}</h2>
<BranchSelection
:branches="branches"
:selectedBranch="selectedBranch"
@branchSelected="onBranchSelected"
@createBranch="createBranch"
@upgradeBranch="upgradeBranch"
ref="branchSelection"
/>
<DirectorInfo
v-if="selectedBranch"
:branchId="selectedBranch.id"
ref="directorInfo"
/>
<SaleSection
v-if="selectedBranch"
:branchId="selectedBranch.id"
ref="saleSection"
/>
<ProductionSection
v-if="selectedBranch"
:branchId="selectedBranch.id"
:products="products"
ref="productionSection"
/>
<StorageSection
v-if="selectedBranch"
:branchId="selectedBranch.id"
ref="storageSection"
/>
<RevenueSection
v-if="selectedBranch"
:products="products"
:calculateProductRevenue="calculateProductRevenue"
:calculateProductProfit="calculateProductProfit"
ref="revenueSection"
/>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import BranchSelection from '@/components/falukant/BranchSelection.vue';
import DirectorInfo from '@/components/falukant/DirectorInfo.vue';
import SaleSection from '@/components/falukant/SaleSection.vue';
import ProductionSection from '@/components/falukant/ProductionSection.vue';
import StorageSection from '@/components/falukant/StorageSection.vue';
import RevenueSection from '@/components/falukant/RevenueSection.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import BranchSelection from '@/components/falukant/BranchSelection.vue';
import DirectorInfo from '@/components/falukant/DirectorInfo.vue';
import SaleSection from '@/components/falukant/SaleSection.vue';
import ProductionSection from '@/components/falukant/ProductionSection.vue';
import StorageSection from '@/components/falukant/StorageSection.vue';
import RevenueSection from '@/components/falukant/RevenueSection.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: "BranchView",
components: {
StatusBar,
BranchSelection,
DirectorInfo,
SaleSection,
ProductionSection,
StorageSection,
RevenueSection,
StatusBar,
BranchSelection,
DirectorInfo,
SaleSection,
ProductionSection,
StorageSection,
RevenueSection,
},
data() {
return {
branches: [],
selectedBranch: null,
products: [],
};
return {
branches: [],
selectedBranch: null,
products: [],
};
},
computed: {
...mapState(['socket', 'daemonSocket']),
...mapState(['socket', 'daemonSocket']),
},
async mounted() {
await this.loadBranches();
const branchId = this.$route.params.branchId;
await this.loadProducts();
if (branchId) {
this.selectedBranch =
this.branches.find(branch => branch.id === parseInt(branchId)) || null;
} else {
this.selectMainBranch();
await this.loadBranches();
const branchId = this.$route.params.branchId;
await this.loadProducts();
if (branchId) {
this.selectedBranch = this.branches.find(
b => b.id === parseInt(branchId, 10)
) || null;
} else {
this.selectMainBranch();
}
// Daemon-Socket
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
// Live-Socket-Events
[
"production_ready",
"stock_change",
"price_update",
"director_death",
"production_started",
"selled_items",
"falukantUpdateStatus",
"falukantBranchUpdate",
"knowledge_update"
].forEach(eventName => {
if (this.socket) {
this.socket.on(eventName, data => this.handleEvent({ event: eventName, ...data }));
}
const events = [
"production_ready",
"stock_change",
"price_update",
"director_death",
"production_started",
"selled_items",
"falukantUpdateStatus",
"falukantBranchUpdate",
];
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
events.forEach(eventName => {
if (this.socket) {
this.socket.on(eventName, (data) => {
this.handleEvent({ event: eventName, ...data });
});
}
});
});
},
beforeUnmount() {
const events = [
"production_ready",
"stock_change",
"price_update",
"director_death",
"production_started",
"selled_items",
"falukantUpdateStatus",
"falukantBranchUpdate",
];
events.forEach(eventName => {
if (this.socket) {
this.socket.off(eventName, this.handleEvent);
}
});
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
[
"production_ready",
"stock_change",
"price_update",
"director_death",
"production_started",
"selled_items",
"falukantUpdateStatus",
"falukantBranchUpdate",
"knowledge_update"
].forEach(eventName => {
if (this.socket) {
this.socket.off(eventName, this.handleEvent);
}
});
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
},
methods: {
async loadBranches() {
try {
const result = await apiClient.get('/api/falukant/branches');
this.branches = result.data.map(branch => ({
id: branch.id,
cityName: branch.region.name,
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
isMainBranch: branch.isMainBranch,
}));
if (!this.selectedBranch) {
this.selectMainBranch();
}
} catch (error) {
console.error('Error loading branches:', error);
}
},
async loadProducts() {
try {
const productsResult = await apiClient.get('/api/falukant/products');
this.products = productsResult.data;
} catch (error) {
console.error('Error loading products:', error);
}
},
handleEvent(event) {
if (event.type === 'branchUpdated') {
this.loadBranches();
}
},
handleDaemonMessage(event) {
const message = JSON.parse(event.data);
},
selectMainBranch() {
const main = this.branches.find(b => b.isMainBranch) || null;
if (main && main !== this.selectedBranch) {
this.selectedBranch = main;
}
},
onBranchSelected(newBranch) {
this.selectedBranch = newBranch;
},
createBranch() {
alert(this.$t('falukant.branch.actions.createAlert'));
},
upgradeBranch() {
if (this.selectedBranch) {
alert(this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
}
},
calculateProductRevenue(product) {
if (!product.knowledges || product.knowledges.length === 0) {
return { absolute: 0, perMinute: 0 };
}
const knowledgeFactor = product.knowledges[0].knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
const perMinute = product.productionTime > 0 ? revenuePerUnit / product.productionTime : 0;
return {
absolute: revenuePerUnit.toFixed(2),
perMinute: perMinute.toFixed(2),
};
},
calculateProductProfit(product) {
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr } = this.calculateProductRevenue(product);
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
const costPerUnit = 6 * product.category;
const profitAbsolute = revenueAbsolute - costPerUnit;
const costPerMinute = product.productionTime > 0 ? costPerUnit / product.productionTime : 0;
const profitPerMinute = parseFloat(revenuePerMinuteStr) - costPerMinute;
return {
absolute: profitAbsolute.toFixed(2),
perMinute: profitPerMinute.toFixed(2),
};
},
// Gemeinsamer Event-Handler für socket-Events
handleEvent(eventData) {
switch (eventData.event || eventData) {
case 'production_ready':
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
break;
case 'stock_change':
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
break;
case 'price_update':
this.$refs.revenueSection && this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
break;
case 'director_death':
this.$refs.directorInfo && this.$refs.directorInfo.loadDirector();
break;
case 'production_started':
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
break;
case 'selled_items':
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
break;
case 'falukantUpdateStatus':
case 'falukantBranchUpdate':
this.$refs.statusBar && this.$refs.statusBar.updateStatus && this.$refs.statusBar.updateStatus(eventData);
this.$refs.productionSection && this.$refs.productionSection.loadProductions();
this.$refs.storageSection && this.$refs.storageSection.loadStorageData();
this.$refs.saleSection && this.$refs.saleSection.loadInventory();
break;
case 'knowledge_update':
this.loadProducts();
this.$refs.revenueSection.products = this.products;
break;
default:
console.log('Unhandled event:', eventData);
}
},
handleDaemonMessage(event) {
if (event.data === "ping") return;
try {
const message = JSON.parse(event.data);
this.handleEvent(message);
} catch (error) {
console.error('Error processing daemon message in BranchView:', error);
}
},
async loadBranches() {
try {
const result = await apiClient.get('/api/falukant/branches');
this.branches = result.data.map(branch => ({
id: branch.id,
cityName: branch.region.name,
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
isMainBranch: branch.isMainBranch,
}));
if (!this.selectedBranch) {
this.selectMainBranch();
}
} catch (error) {
console.error('Error loading branches:', error);
}
},
async loadProducts() {
try {
const productsResult = await apiClient.get('/api/falukant/products');
this.products = productsResult.data;
} catch (error) {
console.error('Error loading products:', error);
}
},
onBranchSelected(newBranch) {
this.selectedBranch = newBranch;
},
async createBranch() {
// Nach erfolgreichem Dialog-Event: neu laden
await this.loadBranches();
},
upgradeBranch() {
if (this.selectedBranch) {
alert(
this.$t(
'falukant.branch.actions.upgradeAlert',
{ branchId: this.selectedBranch.id }
)
);
}
},
selectMainBranch() {
const main = this.branches.find(b => b.isMainBranch) || null;
if (main && main !== this.selectedBranch) {
this.selectedBranch = main;
}
},
calculateProductRevenue(product) {
if (!product.knowledges || product.knowledges.length === 0) {
return { absolute: 0, perMinute: 0 };
}
const knowledgeFactor = product.knowledges[0].knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
const perMinute = product.productionTime > 0
? revenuePerUnit / product.productionTime
: 0;
return {
absolute: revenuePerUnit.toFixed(2),
perMinute: perMinute.toFixed(2),
};
},
calculateProductProfit(product) {
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
= this.calculateProductRevenue(product);
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
const costPerUnit = 6 * product.category;
const profitAbsolute = revenueAbsolute - costPerUnit;
const costPerMinute = product.productionTime > 0
? costPerUnit / product.productionTime
: 0;
const profitPerMinute = parseFloat(revenuePerMinuteStr) - costPerMinute;
return {
absolute: profitAbsolute.toFixed(2),
perMinute: profitPerMinute.toFixed(2),
};
},
handleEvent(eventData) {
switch (eventData.event) {
case 'production_ready':
this.$refs.productionSection?.loadProductions();
this.$refs.storageSection ?.loadStorageData();
this.$refs.saleSection ?.loadInventory();
break;
case 'stock_change':
this.$refs.storageSection ?.loadStorageData();
this.$refs.saleSection ?.loadInventory();
break;
case 'price_update':
this.$refs.revenueSection?.refresh();
break;
case 'director_death':
this.$refs.directorInfo?.loadDirector();
break;
case 'production_started':
this.$refs.productionSection?.loadProductions();
break;
case 'selled_items':
this.$refs.saleSection ?.loadInventory();
this.$refs.storageSection?.loadStorageData();
break;
case 'falukantUpdateStatus':
case 'falukantBranchUpdate':
this.$refs.statusBar?.fetchStatus();
this.$refs.productionSection?.loadProductions();
this.$refs.storageSection ?.loadStorageData();
this.$refs.saleSection ?.loadInventory();
break;
case 'knowledge_update':
this.loadProducts();
this.$refs.revenueSection.products = this.products;
break;
default:
console.log('Unhandled event:', eventData);
}
},
handleDaemonMessage(event) {
if (event.data === 'ping') return;
try {
const message = JSON.parse(event.data);
this.handleEvent(message);
} catch (error) {
console.error('Error processing daemon message:', error);
}
},
},
};
</script>
<style scoped lang="scss">
h2 {
};
</script>
<style scoped lang="scss">
h2 {
padding-top: 20px;
}
</style>
}
</style>

View File

@@ -43,12 +43,13 @@
</tr>
<tr v-if="relationships[0].relationshipType === 'engaged'" colspan="2">
<button @click="jumpToPartyForm">{{ $t('falukant.family.spouse.jumpToPartyForm')
}}</button>
}}</button>
</tr>
</table>
<ul>
<li v-for="characteristic in relationships[0].character2.characterTrait"
:key="characteristic.id">{{ $t(`falukant.character.${characteristic.tr}`) }}</li>
<li v-for="trait in relationships[0].character2.traits" :key="trait.id">
{{ $t(`falukant.character.${trait.tr}`) }}
</li>
</ul>
</div>
<div v-if="relationships[0].relationshipType === 'wooing'">
@@ -64,16 +65,18 @@
</thead>
<tbody>
<tr v-for="gift in gifts" :key="gift.id">
<td><input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId"></td>
<td>
<input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId" />
</td>
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
<td>{{ $t(`falukant.family.spouse.giftAffect.${getEffect(gift)}`) }}</td>
<td>{{ getEffect(gift) }}</td>
<td>{{ formatCost(gift.cost) }}</td>
</tr>
</tbody>
</table>
<div>
<button @click="sendGift" class="button">{{ $t('falukant.family.spouse.wooing.sendGift')
}}</button>
}}</button>
</div>
</div>
</div>
@@ -93,7 +96,7 @@
v-model="selectedProposalId"></td>
<td>{{
$t(`falukant.titles.${proposal.proposedCharacterGender}.${proposal.proposedCharacterNobleTitle}`)
}} {{ proposal.proposedCharacterName }}</td>
}} {{ proposal.proposedCharacterName }}</td>
<td>{{ proposal.proposedCharacterAge }}</td>
<td>{{ formatCost(proposal.cost) }}</td>
</tr>
@@ -123,7 +126,8 @@
{{ child.name }}
</td>
<td v-else>
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism') }}</button>
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism')
}}</button>
</td>
<td>{{ child.age }}</td>
<td>
@@ -308,12 +312,38 @@ export default {
});
},
handleDaemonMessage() {
handleDaemonMessage(event) {
if (event.data === 'ping') {
return;
}
const message = JSON.parse(event.data);
if (message.event === 'children_update') {
this.loadFamilyData();
}
}
},
getEffect(gift) {
// aktueller Partner
const partner = this.relationships[0].character2;
// seine aktuelle Mood-ID
const moodId = partner.mood?.id ?? partner.mood_id;
// 1) Mood-Eintrag finden
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === moodId);
const moodValue = moodEntry ? moodEntry.suitability : 0;
// 2) Trait-Einträge matchen
let highestTraitValue = 0;
for (const trait of partner.traits) {
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
if (charEntry && charEntry.suitability > highestTraitValue) {
highestTraitValue = charEntry.suitability;
}
}
// Durchschnitt, gerundet
return Math.round((moodValue + highestTraitValue) / 2);
},
}
}
</script>

View File

@@ -13,14 +13,15 @@
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' + falukantUser?.character.nobleTitle.labelTr) }}</td>
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' +
falukantUser?.character.nobleTitle.labelTr) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.money') }}</td>
<td>
{{ moneyValue != null
? moneyValue.toLocaleString(locale, { style: 'currency', currency: 'EUR' })
: '---' }}
: '---' }}
</td>
</tr>
<tr>
@@ -170,17 +171,24 @@ export default {
};
},
getHouseStyle() {
if (!this.falukantUser) return {};
console.log(this.falukantUser);
if (!this.falukantUser || !this.falukantUser.userHouse?.houseType) return {};
const imageUrl = '/images/falukant/houses.png';
const housePosition = this.falukantUser.house ? this.falukantUser.house.type.position : 0;
const x = housePosition % 3;
const y = Math.floor(housePosition / 3);
const pos = this.falukantUser.userHouse.houseType.position;
const index = pos - 1;
const columns = 3;
const spriteSize = 300;
const x = (index % columns) * spriteSize;
const y = Math.floor(index / columns) * spriteSize;
return {
backgroundImage: `url(${imageUrl})`,
backgroundPosition: `-${x * 341}px -${y * 341}px`,
backgroundSize: "341px 341px",
width: "114px",
height: "114px",
backgroundPosition: `-${x}px -${y}px`,
backgroundSize: `${columns * spriteSize}px auto`,
width: `300px`,
height: `300px`,
border: '1px solid #ccc',
borderRadius: '4px',
imageRendering: 'crisp-edges',
};
},
getAgeColor(age) {
@@ -321,12 +329,10 @@ export default {
border: 1px solid #ccc;
border-radius: 4px;
background-repeat: no-repeat;
background-size: cover;
image-rendering: crisp-edges;
}
h2 {
padding-top: 20px;
}
</style>

View File

@@ -0,0 +1,405 @@
<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>
</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>
</tr>
<tr v-if="!currentPositions.length">
<td colspan="3">{{ $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" />
</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;
this.selectedApplications = [];
} 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>

View File

@@ -2,89 +2,84 @@
<h2 class="link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
<h3 v-if="forumTopic">{{ forumTopic }}</h3>
<ul class="messages">
<li v-for="message in messages">
<li v-for="message in messages" :key="message.id">
<div v-html="message.text"></div>
<div class="footer">
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">{{
message.lastMessageUser.username }}</span>
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">
{{ message.lastMessageUser.username }}
</span>
<span>{{ new Date(message.createdAt).toLocaleString() }}</span>
</div>
</li>
</ul>
<editor v-model="newContent" :init="tinymceInitOptions" :api-key="apiKey"
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
<div class="editor-container">
<EditorContent :editor="editor" class="editor" />
</div>
<button @click="saveNewMessage">{{ $t('socialnetwork.forum.createNewMesssage') }}</button>
</template>
<script>
import apiClient from '../../utils/axios';
import TinyMCEEditor from '@tinymce/tinymce-vue';
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import apiClient from '../../utils/axios'
export default {
name: 'ForumTopicView',
components: {
editor: TinyMCEEditor,
EditorContent,
},
data() {
return {
forumTopicId: String,
forumTopicId: '',
forumTopic: null,
forumName: null,
forumId: 0,
newContent: '',
apiKey: import.meta.env.VITE_TINYMCE_API_KEY ?? '',
tinymceInitOptions: {
script_url: '/tinymce/tinymce.min.js',
height: 300,
menubar: true,
plugins: [
'lists', 'link',
'searchreplace', 'visualblocks', 'code',
'insertdatetime', 'table'
],
toolbar:
'undo redo cut copy paste | bold italic forecolor backcolor fontfamily fontsize| \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | link visualblocks code',
contextmenu: 'link image table',
menubar: 'edit format table',
promotion: false,
},
messages: [],
editor: null,
}
},
async mounted() {
mounted() {
this.forumTopicId = this.$route.params.id;
this.loadForumTopic();
this.editor = new Editor({
extensions: [StarterKit],
content: '',
});
},
beforeUnmount() {
if (this.editor) {
this.editor.destroy();
}
},
methods: {
async loadForumTopic() {
try {
console.log(this.forumTopicId);
const url = `/api/forum/topic/${this.forumTopicId}`;
console.log('url', url);
const response = await apiClient.get(url);
const response = await apiClient.get(`/api/forum/topic/${this.forumTopicId}`);
this.setContent(response.data);
} catch (error) {
console.error(error);
}
},
setContent(responseData) {
this.forumTopic = responseData.title;
this.forumName = responseData.forum.name;
this.forumId = responseData.forum.id;
this.messages = responseData.messages;
setContent(data) {
this.forumTopic = data.title;
this.forumName = data.forum.name;
this.forumId = data.forum.id;
this.messages = data.messages;
},
async openProfile(id) {
this.$root.$refs.userProfileDialog.userId = id;
this.$root.$refs.userProfileDialog.open();
},
async saveNewMessage() {
const content = this.editor ? this.editor.getHTML() : '';
if (!content.trim()) return;
try {
const url = `/api/forum/topic/${this.forumTopicId}/message`;
const response = await apiClient.post(url, {
content: this.newContent,
});
this.newContent = '';
const response = await apiClient.post(url, { content });
this.editor.commands.clearContent();
this.setContent(response.data);
} catch (error) {
console.error(error);
@@ -96,7 +91,6 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.messages {
list-style-type: none;
@@ -117,11 +111,24 @@ export default {
display: flex;
}
.messages>li>.footer>span:first-child {
.messages > li > .footer > span:first-child {
flex: 1;
}
.messages > li > .footer > span:last-child {
text-align: right;
}
</style>
.editor-container {
margin-top: 1rem;
border: 1px solid #ccc;
padding: 10px;
min-height: 200px;
background-color: white;
}
.editor {
min-height: 150px;
outline: none;
}
</style>

View File

@@ -11,71 +11,27 @@
<input type="text" v-model="newTitle" />
</label>
</div>
<editor v-model="newContent" :init="tinymceInitOptions" :api-key="apiKey"
tinymce-script-src="/tinymce/tinymce.min.js"></editor>
<div class="editor-container">
<EditorContent :editor="editor" class="editor" />
</div>
<button @click="saveNewTopic">{{ $t('socialnetwork.forum.createNewTopic') }}</button>
</div>
<div v-else-if="titles.length > 0">
<div class="pagination">
<button @click="goToPage(1)" v-if="page != 1">&laquo; {{ $t('socialnetwork.forum.pagination.first')
}}</button>
<button @click="goToPage(page - 1)" v-if="page != 1">&lsaquo; {{
$t('socialnetwork.forum.pagination.previous') }}</button>
<span>{{ $t('socialnetwork.forum.pagination.page').replace("<<page>>", page).replace("<<of>>", totalPages)
}}</span>
<button @click="goToPage(page + 1)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.next')
}}
&rsaquo;</button>
<button @click="goToPage(totalPages)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.last')
}}
&raquo;</button>
</div>
<table>
<thead>
<tr>
<th>{{ $t('socialnetwork.forum.topic') }}</th>
<th>{{ $t('socialnetwork.forum.createdBy') }}</th>
<th>{{ $t('socialnetwork.forum.createdAt') }}</th>
<th>{{ $t('socialnetwork.forum.reactions') }}</th>
<th>{{ $t('socialnetwork.forum.lastReaction') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="title in titles">
<td><span class="link" @click="openTopic(title.id)">{{ title.title }}</span></td>
<td><span class="link" @click="openProfile(title.createdByHash)">{{ title.createdBy }}</span></td>
<td>{{ new Date(title.createdAt).toLocaleString() }}</td>
<td>{{ title.numberOfItems }}</td>
<td>{{ new Date(title.lastMessageDate).toLocaleString() }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button @click="goToPage(1)" v-if="page != 1">&laquo; {{ $t('socialnetwork.forum.pagination.first')
}}</button>
<button @click="goToPage(page - 1)" v-if="page != 1">&lsaquo; {{
$t('socialnetwork.forum.pagination.previous') }}</button>
<span>{{ $t('socialnetwork.forum.pagination.page').replace("<<page>>", page).replace("<<of>>", totalPages)
}}</span>
<button @click="goToPage(page + 1)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.next')
}}
&rsaquo;</button>
<button @click="goToPage(totalPages)" v-if="page != totalPages">{{ $t('socialnetwork.forum.pagination.last')
}}
&raquo;</button>
</div>
<!-- PAGINATION + TABLE bleibt unverändert -->
<!-- ... -->
</div>
<div v-else>{{ $t('socialnetwork.forum.noTitles') }}</div>
</template>
<script>
import apiClient from '../../utils/axios';
import TinyMCEEditor from '@tinymce/tinymce-vue';
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import apiClient from '../../utils/axios'
export default {
name: 'ForumView',
components: {
editor: TinyMCEEditor,
EditorContent,
},
computed: {
totalPages() {
@@ -91,30 +47,20 @@ export default {
titles: [],
inCreation: false,
newTitle: '',
newContent: '',
apiKey: import.meta.env.VITE_TINYMCE_API_KEY ?? '',
tinymceInitOptions: {
script_url: '/tinymce/tinymce.min.js',
height: 300,
menubar: true,
plugins: [
'lists', 'link',
'searchreplace', 'visualblocks', 'code',
'insertdatetime', 'table'
],
toolbar:
'undo redo cut copy paste | bold italic forecolor backcolor fontfamily fontsize| \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | link visualblocks code',
contextmenu: 'link image table',
menubar: 'edit format table',
promotion: false,
},
editor: null,
}
},
async mounted() {
this.forumId = this.$route.params.id;
await this.loadForum();
this.editor = new Editor({
extensions: [StarterKit],
content: '',
});
},
beforeUnmount() {
if (this.editor) this.editor.destroy();
},
methods: {
async loadForum() {
@@ -123,12 +69,16 @@ export default {
},
createNewTopic() {
this.inCreation = !this.inCreation;
if (this.inCreation && this.editor) {
this.editor.commands.setContent('');
}
},
async saveNewTopic() {
const content = this.editor ? this.editor.getHTML() : '';
const response = await apiClient.post('/api/forum/topic', {
forumId: this.forumId,
title: this.newTitle,
content: this.newEntryContent
content,
});
this.setData(response.data);
this.inCreation = false;
@@ -171,6 +121,19 @@ export default {
flex: 1;
}
.editor-container {
margin: 1em 0;
border: 1px solid #ccc;
padding: 10px;
min-height: 200px;
background-color: white;
}
.editor {
min-height: 150px;
outline: none;
}
.pagination {
display: flex;
justify-content: center;
@@ -185,4 +148,4 @@ export default {
.pagination span {
padding: 0.5em;
}
</style>
</style>