Files
yourpart3/frontend/src/views/falukant/OverviewView.vue
Torsten Schulz (local) 33aa2ddd45 Refactor OverviewView and NoLoginView to integrate Character3D component
- Replaced avatar display logic in OverviewView with a 3D character representation based on user gender and age.
- Updated NoLoginView to utilize Character3D for displaying mascots, enhancing visual consistency.
- Removed outdated avatar positioning logic and related computed properties for improved code clarity and maintainability.
- Adjusted CSS styles for better layout and responsiveness of character displays.
2026-01-22 11:06:38 +01:00

426 lines
15 KiB
Vue

<template>
<div>
<StatusBar />
<h2>{{ $t('falukant.overview.title') }}</h2>
<!-- Erben-Auswahl wenn kein Charakter vorhanden -->
<div v-if="!falukantUser?.character" class="heir-selection-container">
<h3>{{ $t('falukant.overview.heirSelection.title') }}</h3>
<p>{{ $t('falukant.overview.heirSelection.description') }}</p>
<div v-if="loadingHeirs" class="loading">{{ $t('falukant.overview.heirSelection.loading') }}</div>
<div v-else-if="potentialHeirs.length === 0" class="no-heirs">
{{ $t('falukant.overview.heirSelection.noHeirs') }}
</div>
<div v-else class="heirs-list">
<div v-for="heir in potentialHeirs" :key="heir.id" class="heir-card">
<div class="heir-info">
<div class="heir-name">
{{ $t(`falukant.titles.${heir.gender}.noncivil`) }}
{{ heir.definedFirstName.name }} {{ heir.definedLastName.name }}
</div>
<div class="heir-age">{{ $t('falukant.overview.metadata.age') }}: {{ heir.age }}</div>
</div>
<button @click="selectHeir(heir.id)" class="select-heir-button">
{{ $t('falukant.overview.heirSelection.select') }}
</button>
</div>
</div>
</div>
<!-- Normale Übersicht wenn Charakter vorhanden -->
<div v-else class="overviewcontainer">
<div>
<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>
{{ 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>
<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>
<p v-else>{{ $t('falukant.branch.production.noProductions') }}</p>
</div>
<div>
<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>
<p v-else>{{ $t('falukant.branch.sale.noInventory') }}</p>
</div>
<div>
<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>
<div v-if="falukantUser?.character" class="imagecontainer">
<div class="character-3d-wrapper">
<Character3D
:gender="falukantUser.character.gender"
:age="falukantUser.character.age"
/>
</div>
<div :style="getHouseStyle" class="house"></div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import Character3D from '@/components/Character3D.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: 'FalukantOverviewView',
components: {
StatusBar,
Character3D,
},
data() {
return {
falukantUser: null,
allStock: [],
productions: [],
potentialHeirs: [],
loadingHeirs: false,
};
},
computed: {
...mapState(['socket']),
getHouseStyle() {
console.log(this.falukantUser);
if (!this.falukantUser || !this.falukantUser.userHouse?.houseType) return {};
const imageUrl = '/images/falukant/houses.png';
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}px -${y}px`,
backgroundSize: `${columns * spriteSize}px auto`,
width: `300px`,
height: `300px`,
border: '1px solid #ccc',
borderRadius: '4px',
imageRendering: 'crisp-edges',
};
},
moneyValue() {
const m = this.falukantUser?.money;
return typeof m === 'string' ? parseFloat(m) : m;
},
locale() {
return window.navigator.language || 'en-US';
},
},
watch: {
socket(newSocket) {
if (newSocket) {
this.setupSocketEvents();
}
}
},
async mounted() {
await this.fetchFalukantUser();
if (!this.falukantUser?.character) {
await this.fetchPotentialHeirs();
} else {
await this.fetchAllStock();
await this.fetchProductions();
}
// Daemon WebSocket deaktiviert - verwende Socket.io für alle Events
this.setupSocketEvents();
},
beforeUnmount() {
if (this.socket) {
this.socket.off("falukantUserUpdated", this.fetchFalukantUser);
this.socket.off("production_ready", this.handleProductionReadyEvent);
}
// Daemon WebSocket deaktiviert - keine Cleanup nötig
},
methods: {
setupSocketEvents() {
if (this.socket) {
this.socket.on("falukantUserUpdated", this.fetchFalukantUser);
this.socket.on("production_ready", this.handleProductionReadyEvent);
this.socket.on("falukantUpdateStatus", (data) => {
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
});
this.socket.on("falukantBranchUpdate", (data) => {
this.handleEvent({ event: 'falukantBranchUpdate', ...data });
});
} else {
// Versuche es nach kurzer Verzögerung erneut
setTimeout(() => {
this.setupSocketEvents();
}, 1000);
}
},
handleEvent(eventData) {
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantBranchUpdate':
this.fetchFalukantUser();
break;
}
},
async fetchFalukantUser() {
const falukantUser = await apiClient.get('/api/falukant/user');
if (!falukantUser.data) {
this.$router.push({ name: 'FalukantCreate' });
return;
}
this.falukantUser = falukantUser.data;
},
async fetchAllStock() {
const response = await apiClient.get('/api/falukant/stockoverview');
const rawData = response.data;
const aggregated = {};
for (const item of rawData) {
const key = `${item.regionName}__${item.productLabelTr}`;
if (!aggregated[key]) {
aggregated[key] = {
regionName: item.regionName,
productLabelTr: item.productLabelTr,
quantity: 0,
};
}
aggregated[key].quantity += item.quantity;
}
this.allStock = Object.values(aggregated);
},
handleProductionReadyEvent() {
this.fetchAllStock();
this.fetchProductions();
},
openBranch(branchId) {
this.$router.push({ name: 'BranchView', params: { branchId } });
},
async fetchProductions() {
try {
const response = await apiClient.get('/api/falukant/productions');
this.productions = response.data;
} catch (error) {
console.error('Error fetching productions:', error);
}
},
formatDate(timestamp) {
return new Date(timestamp).toLocaleString();
},
async fetchPotentialHeirs() {
// Prüfe sowohl mainBranchRegion.id als auch mainBranchRegionId
const regionId = this.falukantUser?.mainBranchRegion?.id || this.falukantUser?.mainBranchRegionId;
if (!regionId) {
console.error('No main branch region found', this.falukantUser);
this.potentialHeirs = [];
return;
}
this.loadingHeirs = true;
try {
const response = await apiClient.get('/api/falukant/heirs/potential');
this.potentialHeirs = response.data || [];
if (this.potentialHeirs.length === 0) {
console.warn('No potential heirs returned from API');
}
} catch (error) {
console.error('Error fetching potential heirs:', error);
console.error('Error details:', error.response?.data || error.message);
this.potentialHeirs = [];
} finally {
this.loadingHeirs = false;
}
},
async selectHeir(heirId) {
try {
await apiClient.post('/api/falukant/heirs/select', { heirId });
// Lade User-Daten neu
await this.fetchFalukantUser();
if (this.falukantUser?.character) {
await this.fetchAllStock();
await this.fetchProductions();
}
} catch (error) {
console.error('Error selecting heir:', error);
alert(this.$t('falukant.overview.heirSelection.error'));
}
},
},
};
</script>
<style scoped lang="scss">
.overviewcontainer {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 5px;
}
.overviewcontainer>div {
border: 1px solid #ccc;
padding: 5px;
border-radius: 4px;
}
.imagecontainer {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
}
.character-3d-wrapper {
width: 300px;
height: 400px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #fdf1db;
display: flex;
justify-content: center;
align-items: center;
}
.house {
border: 1px solid #ccc;
border-radius: 4px;
background-repeat: no-repeat;
image-rendering: crisp-edges;
}
h2 {
padding-top: 20px;
}
.heir-selection-container {
border: 2px solid #dc3545;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
background-color: #fff3cd;
}
.heir-selection-container h3 {
margin-top: 0;
color: #856404;
}
.heirs-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
margin-top: 20px;
}
.heir-card {
border: 1px solid #ccc;
border-radius: 4px;
padding: 15px;
background-color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.heir-info {
flex: 1;
}
.heir-name {
font-weight: bold;
margin-bottom: 5px;
}
.heir-age {
color: #666;
font-size: 0.9em;
}
.select-heir-button {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.select-heir-button:hover {
background-color: #218838;
}
.loading, .no-heirs {
text-align: center;
padding: 20px;
color: #666;
}
</style>