Files
yourpart3/frontend/src/views/falukant/HouseView.vue
Torsten Schulz (local) 06ea259dc9 Add Falukant region and transport management features
- Implemented new endpoints in AdminController for managing Falukant regions, including fetching, updating, and deleting region distances.
- Enhanced the FalukantService with methods for retrieving region distances and handling upsert operations.
- Updated the router to expose new routes for region management and transport creation.
- Introduced a transport management interface in the frontend, allowing users to create and manage transports between branches.
- Added localization for new transport-related terms and improved the vehicle management interface to include transport options.
- Enhanced the database initialization logic to support new region and transport models.
2025-11-26 16:44:27 +01:00

321 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="house-view">
<StatusBar />
<h2>{{ $t('falukant.house.title') }}</h2>
<div class="existing-house">
<div :style="houseType ? houseStyle(houseType.position, 341) : {}" class="house"></div>
<div class="status-panel">
<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>
</div>
<div class="buyable-houses">
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
<div class="houses-list">
<div v-for="house in buyableHouses" :key="house.id" class="house-item">
<div :style="house.houseType ? houseStyle(house.houseType.position, 114) : {}"
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>
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
</div>
<button @click="buyHouse(house.id)">
{{ $t('falukant.house.buy') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: 'HouseView',
components: { StatusBar },
data() {
return {
userHouse: null,
houseType: {},
status: {},
buyableHouses: [],
currency: '€'
};
},
computed: {
...mapState(['socket']),
allRenovated() {
return Object.values(this.status).every(v => v >= 100);
}
},
methods: {
async loadData() {
try {
const userRes = await apiClient.get('/api/falukant/houses');
this.userHouse = userRes.data;
this.houseType = this.userHouse.houseType;
const { roofCondition, wallCondition, floorCondition, windowCondition } = this.userHouse;
this.status = { roofCondition, wallCondition, floorCondition, windowCondition };
const buyRes = await apiClient.get('/api/falukant/houses/buyable');
this.buyableHouses = buyRes.data;
} catch (err) {
console.error('Error loading house data', err);
}
},
conditionLabel(value) {
const v = Number(value) || 0;
if (v >= 95) return 'Ausgezeichnet'; // 95100
if (v >= 72) return 'Sehr gut'; // 7294
if (v >= 54) return 'Gut'; // 5471
if (v >= 39) return 'Mäßig'; // 3953
if (v >= 22) return 'Schlecht'; // 2238
if (v >= 6) return 'Sehr schlecht'; // 621
if (v >= 1) return 'Katastrophal'; // 15
return 'Unbekannt';
},
houseStyle(position, picSize) {
const columns = 3;
const size = picSize;
const index = position - 1;
const x = (index % columns) * size;
const y = Math.floor(index / columns) * size;
return {
backgroundImage: 'url("/images/falukant/houses.png")',
backgroundPosition: `-${x}px -${y}px`,
backgroundSize: `${columns * size}px auto`
};
},
formatPrice(value) {
return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
},
getRenovationCost(key, value) {
if (!this.userHouse || !this.userHouse.houseType) return this.formatPrice(0);
const base = this.userHouse.houseType.cost || 0;
const weights = { roofCondition: 0.25, wallCondition: 0.25, floorCondition: 0.25, windowCondition: 0.25 };
const weight = weights[key] || 0;
const missing = 100 - value;
const cost = (missing / 100) * base * weight;
return this.formatPrice(cost);
},
getAllRenovationCost() {
const total = Object.keys(this.status).reduce((sum, k) => {
const raw = parseFloat(this.getRenovationCost(k, this.status[k]).replace(/\./g, '').replace(',', '.'));
return sum + (isNaN(raw) ? 0 : raw);
}, 0);
return this.formatPrice(total * 0.8);
},
getWorth() {
const vals = Object.values(this.status);
if (!vals.length || !this.houseType) return this.formatPrice(0);
const avg = vals.reduce((s, v) => s + v, 0) / vals.length;
const price = this.houseType.cost || 0;
return this.formatPrice(price * avg / 100 * 0.8);
},
buyCost(house) {
if (!house || !house.houseType) return this.formatPrice(0);
const avg = (house.roofCondition + house.wallCondition + house.floorCondition + house.windowCondition) / 4;
return this.formatPrice(house.houseType.cost * avg / 100);
},
async renovate(key) {
try {
await apiClient.post('/api/falukant/houses/renovate', { element: key });
await this.loadData();
} catch (err) {
console.error('Error renovating', err);
}
},
async renovateAll() {
try {
await apiClient.post('/api/falukant/houses/renovate-all');
await this.loadData();
} catch (err) {
console.error('Error renovating all', err);
}
},
async sellHouse() {
try {
await apiClient.post('/api/falukant/houses/sell');
await this.loadData();
} catch (err) {
console.error('Error selling house', err);
}
},
async buyHouse(id) {
try {
await apiClient.post('/api/falukant/houses', { houseId: id });
await this.loadData();
} catch (err) {
console.error('Error buying house', err);
}
},
handleDaemonMessage(evt) {
try {
const msg = JSON.parse(evt.data);
if (msg.event === 'houseupdated') this.loadData();
} catch { }
},
setupSocketEvents() {
if (this.socket) {
this.socket.on('falukantHouseUpdate', (data) => {
this.handleEvent({ event: 'falukantHouseUpdate', ...data });
});
this.socket.on('falukantUpdateStatus', (data) => {
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
});
} else {
setTimeout(() => this.setupSocketEvents(), 1000);
}
},
handleEvent(eventData) {
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantHouseUpdate':
this.loadData();
break;
}
}
},
async mounted() {
await this.loadData();
this.setupSocketEvents();
},
beforeUnmount() {
if (this.socket) {
this.socket.off('falukantHouseUpdate', this.loadData);
this.socket.off('falukantUpdateStatus', this.loadData);
}
}
};
</script>
<style scoped>
.house-view {
display: flex;
flex-direction: column;
gap: 20px;
}
h2 {
padding-top: 20px;
margin: 0 0 10px;
}
.existing-house {
display: flex;
gap: 20px;
}
.house {
width: 341px;
height: 341px;
background-repeat: no-repeat;
image-rendering: crisp-edges;
border: 1px solid #ccc;
border-radius: 4px;
}
.status-panel {
flex: 1;
}
.buyable-houses {
display: flex;
flex-direction: column;
gap: 10px;
}
.houses-list {
display: flex;
flex-direction: column;
/* vertical list */
gap: 20px;
max-height: 400px;
overflow-y: auto;
/* vertical scroll if needed */
}
.house-item {
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
}
.house-preview {
width: 100px;
height: 100px;
background-repeat: no-repeat;
image-rendering: crisp-edges;
border: 1px solid #ccc;
border-radius: 4px;
background-size: contain;
/* scale image to container */
background-position: center;
/* center sprite */
}
table {
width: 100%;
border-collapse: collapse;
}
table th,
table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
button {
padding: 6px 12px;
cursor: pointer;
}
</style>