- 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.
321 lines
11 KiB
Vue
321 lines
11 KiB
Vue
<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'; // 95–100
|
||
if (v >= 72) return 'Sehr gut'; // 72–94
|
||
if (v >= 54) return 'Gut'; // 54–71
|
||
if (v >= 39) return 'Mäßig'; // 39–53
|
||
if (v >= 22) return 'Schlecht'; // 22–38
|
||
if (v >= 6) return 'Sehr schlecht'; // 6–21
|
||
if (v >= 1) return 'Katastrophal'; // 1–5
|
||
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>
|