- Changed error throwing in FalukantService to use PreconditionError for better clarity. - Added translations for "too close" error in both German and English locales. - Improved user feedback in HealthView by displaying error messages in a dialog upon measure execution failure.
210 lines
6.9 KiB
Vue
210 lines
6.9 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);
|
|
const title = this.$t('falukant.healthview.title');
|
|
const remoteMsg = err?.response?.data?.error || err?.message || String(err);
|
|
this.$root.$refs.messageDialog?.open(remoteMsg, title);
|
|
}
|
|
},
|
|
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>
|