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

224 lines
7.8 KiB
Vue

<template>
<div>
<StatusBar />
<h2>{{ $t('falukant.healthview.title') }}</h2>
<div class="content-container">
<div class="info-panel">
<p>{{ $t('falukant.healthview.age') }}: {{ age }}</p>
<p>{{ $t('falukant.healthview.status') }}: {{ healthState }}</p>
</div>
<div class="measures-panel">
<h3>{{ $t('falukant.healthview.measuresTaken') }}</h3>
<table class="measures-table">
<thead>
<tr>
<th>{{ $t('falukant.healthview.measure') }}</th>
<th>{{ $t('falukant.healthview.date') }}</th>
<th>{{ $t('falukant.healthview.success') }}</th>
<th>{{ $t('falukant.healthview.cost') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in measuresTaken" :key="entry.id">
<td>{{ $t(`falukant.healthview.measures.${entry.tr}`) }}</td>
<td>{{ formatDate(entry.createdAt) }}</td>
<td>{{ entry.success }}</td>
<td>{{ formatPrice(entry.cost) }}</td>
</tr>
</tbody>
</table>
<div class="actions">
<label>
{{ $t('falukant.healthview.selectMeasure') }}:
<select v-model="selectedTr">
<option value="" disabled>{{ $t('falukant.healthview.choose') }}</option>
<option v-for="m in availableMeasures" :key="m.tr" :value="m.tr">
{{ $t(`falukant.healthview.measures.${m.tr}`) }} ({{ formatPrice(m.cost) }})
</option>
</select>
</label>
<button @click="performMeasure" :disabled="!selectedMeasure">
{{ $t('falukant.healthview.perform') }}
<span v-if="selectedMeasure"> ({{ formatPrice(selectedMeasure.cost) }})</span>
</button>
</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: 'HealthView',
components: { StatusBar },
data() {
return {
age: 0,
healthStatus: 0,
measuresTaken: [],
availableMeasures: [],
selectedTr: '',
};
},
computed: {
...mapState(['socket']),
/**
* Selected measure object based on selectedTr
*/
selectedMeasure() {
return this.availableMeasures.find(m => m.tr === this.selectedTr) || null;
},
/**
* Health state translation key based on status
*/
healthState() {
if (this.healthStatus > 90) return this.$t('falukant.health.amazing');
if (this.healthStatus > 75) return this.$t('falukant.health.good');
if (this.healthStatus > 50) return this.$t('falukant.health.normal');
if (this.healthStatus > 25) return this.$t('falukant.health.bad');
return this.$t('falukant.health.very_bad');
}
},
async mounted() {
await this.loadHealthData();
this.setupSocketEvents();
},
beforeUnmount() {
if (this.socket) {
this.socket.off('falukantUpdateStatus', this.loadHealthData);
}
},
methods: {
setupSocketEvents() {
if (this.socket) {
this.socket.on('falukantUpdateStatus', (data) => {
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
});
} else {
setTimeout(() => this.setupSocketEvents(), 1000);
}
},
handleEvent(eventData) {
switch (eventData.event) {
case 'falukantUpdateStatus':
this.loadHealthData();
break;
}
},
async loadHealthData() {
try {
const { data } = await apiClient.get('/api/falukant/health');
this.age = data.age;
this.healthStatus = data.health;
this.measuresTaken = data.history;
this.availableMeasures = data.healthActivities;
} catch (err) {
console.error('Error loading health data', err);
}
},
formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString();
},
async performMeasure() {
if (!this.selectedMeasure) return;
try {
const { data } = await apiClient.post('/api/falukant/health', {
measureTr: this.selectedTr
});
// Feedback via global message dialog
const title = this.$t('falukant.healthview.title');
const body = data?.delta != null
? `${this.$t(`falukant.healthview.measures.${this.selectedTr}`)}: ${data.delta > 0 ? '+' : ''}${data.delta}`
: this.$t('message.success');
this.$root.$refs.messageDialog?.open(body, title);
await this.loadHealthData();
this.selectedTr = '';
} catch (err) {
console.error('Error performing measure', err);
if (err?.response?.status === 412) {
const retryAtIso = err.response?.data?.retryAt;
const code = err.response?.data?.error || err.response?.data?.message;
if (retryAtIso) {
const retryStr = new Date(retryAtIso).toLocaleString(navigator.language, {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
const baseMsg = this.$t(`falukant.healthview.errors.${code}`);
this.$root.$refs.errorDialog?.open(`${baseMsg}${this.$t('falukant.healthview.nextMeasureAt')}: ${retryStr}`);
} else {
this.$root.$refs.errorDialog?.open(this.$t(`falukant.healthview.errors.${code}`));
}
} else {
const code = err?.response?.data?.error || err?.message || 'generic';
this.$root.$refs.errorDialog?.open(this.$t(`falukant.healthview.errors.${code}`) || this.$t('falukant.healthview.errors.generic'));
}
}
},
handleDaemonMessage(evt) {
if (evt.data === 'ping') return;
const msg = JSON.parse(evt.data);
if (msg.event === 'healthupdated') {
this.loadHealthData();
}
},
formatPrice(value) {
return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
},
}
};
</script>
<style scoped>
h2 {
padding-top: 20px;
margin: 0 0 10px;
}
.content-container {
display: flex;
gap: 20px;
}
.info-panel {
flex: 1;
padding: 10px;
}
.measures-panel {
flex: 2;
padding: 10px;
border-left: 1px solid #ccc;
}
.measures-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
.measures-table th,
.measures-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
button {
padding: 6px 12px;
cursor: pointer;
}
</style>