Files
yourpart3/frontend/src/views/falukant/HouseView.vue

371 lines
12 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 surface-card">
<h3>{{ $t('falukant.house.statusreport') }}</h3>
<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>
<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>
<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)">
{{ $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;
padding: 18px;
}
.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: 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 {
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 */
}
.house-info {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.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>