Files
yourpart3/frontend/src/views/falukant/HealthView.vue
Torsten Schulz (local) fb025f4090 Refactor error handling in FalukantService and enhance user feedback in HealthView
- 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.
2025-12-23 12:20:37 +01:00

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>