Files
yourpart3/frontend/src/dialogues/falukant/MoneyHistoryGraphDialog.vue

268 lines
7.3 KiB
Vue

<template>
<DialogWidget
ref="dialog"
:title="$t('falukant.moneyHistory.graph.title')"
:isTitleTranslated="true"
:show-close="true"
:buttons="[{ text: 'message.close', action: 'close' }]"
:modal="true"
width="600px"
height="400px"
name="MoneyHistoryGraphDialog"
>
<div class="graph-content">
<div class="graph-controls">
<label>
{{ $t('falukant.moneyHistory.graph.range.label') }}
<select v-model="graphRange" @change="loadGraphData">
<option value="today">{{ $t('falukant.moneyHistory.graph.range.today') }}</option>
<option value="24h">{{ $t('falukant.moneyHistory.graph.range.24h') }}</option>
<option value="week">{{ $t('falukant.moneyHistory.graph.range.week') }}</option>
<option value="month">{{ $t('falukant.moneyHistory.graph.range.month') }}</option>
<option value="year">{{ $t('falukant.moneyHistory.graph.range.year') }}</option>
<option value="all">{{ $t('falukant.moneyHistory.graph.range.all') }}</option>
</select>
</label>
</div>
<div v-if="graphLoading" class="graph-loading">
{{ $t('falukant.moneyHistory.graph.loading') }}
</div>
<div v-else-if="!graphData.length" class="graph-no-data">
{{ $t('falukant.moneyHistory.graph.noData') }}
</div>
<div v-else class="graph-container">
<svg viewBox="0 0 100 50" preserveAspectRatio="none" class="graph-svg">
<!-- Y-Achse Beschriftungen (Geldbeträge) -->
<g class="y-axis-labels">
<text
v-for="(label, index) in yAxisLabels"
:key="'y-' + index"
:x="2"
:y="label.y"
class="axis-label y-label"
text-anchor="start"
>
{{ label.text }}
</text>
</g>
<!-- X-Achse Beschriftungen (Zeit) -->
<g class="x-axis-labels">
<text
v-for="(label, index) in xAxisLabels"
:key="'x-' + index"
:x="label.x"
:y="47"
class="axis-label x-label"
text-anchor="middle"
>
{{ label.text }}
</text>
</g>
<!-- Graph-Linie -->
<polyline
:points="graphPolylinePoints"
fill="none"
stroke="#F9A22C"
stroke-width="0.3"
/>
<!-- Achsenlinien -->
<line x1="8" y1="2" x2="8" y2="42" stroke="#666" stroke-width="0.2" />
<line x1="8" y1="42" x2="98" y2="42" stroke="#666" stroke-width="0.2" />
</svg>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue'
import apiClient from '@/utils/axios.js'
export default {
name: 'MoneyHistoryGraphDialog',
components: {
DialogWidget,
},
data() {
return {
graphRange: '24h',
graphData: [],
graphLoading: false,
_minX: null,
_maxX: null,
_minY: null,
_maxY: null,
}
},
computed: {
graphPolylinePoints() {
if (!this.graphData.length) return ''
const xs = this.graphData.map(d => new Date(d.time).getTime())
const ys = this.graphData.map(d => {
if (d.moneyAfter != null) return Number(d.moneyAfter)
if (d.moneyBefore != null) return Number(d.moneyBefore)
return 0
})
this._minX = Math.min(...xs)
this._maxX = Math.max(...xs)
this._minY = Math.min(...ys)
this._maxY = Math.max(...ys)
const spanX = this._maxX - this._minX || 1
const spanY = this._maxY - this._minY || 1
return xs.map((x, i) => {
const normX = 8 + ((x - this._minX) / spanX) * 90 // 8-98 Bereich für X
const normY = 42 - ((ys[i] - this._minY) / spanY) * 38 // 2-42 Bereich für Y
return `${normX.toFixed(2)},${normY.toFixed(2)}`
}).join(' ')
},
yAxisLabels() {
if (!this.graphData.length || !this._minY || !this._maxY) return []
const labels = []
const numLabels = 5
const span = this._maxY - this._minY || 1
for (let i = 0; i <= numLabels; i++) {
const value = this._minY + (span * i / numLabels)
const y = 42 - (i / numLabels) * 38
labels.push({
y: y + 1.5, // Zentrierung
text: this.formatMoney(value)
})
}
return labels
},
xAxisLabels() {
if (!this.graphData.length || !this._minX || !this._maxX) return []
const labels = []
const numLabels = 5
const span = this._maxX - this._minX || 1
for (let i = 0; i <= numLabels; i++) {
const timestamp = this._minX + (span * i / numLabels)
const x = 8 + (i / numLabels) * 90
const date = new Date(timestamp)
labels.push({
x: x,
text: this.formatDate(date)
})
}
return labels
},
},
methods: {
open() {
this.$refs.dialog.open()
this.loadGraphData()
},
async loadGraphData() {
this.graphLoading = true
try {
const response = await apiClient.post('/api/falukant/moneyhistory/graph', {
range: this.graphRange,
})
this.graphData = Array.isArray(response.data) ? response.data : []
// Reset min/max für computed properties
this._minX = null
this._maxX = null
this._minY = null
this._maxY = null
} catch (error) {
console.error('Error loading money history graph data:', error)
this.graphData = []
} finally {
this.graphLoading = false
}
},
formatMoney(amount) {
return new Intl.NumberFormat(navigator.language, {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
style: 'currency',
currency: 'EUR'
}).format(amount)
},
formatDate(date) {
const now = new Date()
const diffMs = now - date
const diffHours = diffMs / (1000 * 60 * 60)
if (diffHours < 24) {
// Heute: nur Uhrzeit
return date.toLocaleTimeString(navigator.language, { hour: '2-digit', minute: '2-digit' })
} else if (diffHours < 48) {
// Gestern: "Gestern" + Uhrzeit
return this.$t('falukant.moneyHistory.graph.yesterday') + ' ' + date.toLocaleTimeString(navigator.language, { hour: '2-digit', minute: '2-digit' })
} else {
// Älter: Datum + Uhrzeit
return date.toLocaleString(navigator.language, {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
},
},
}
</script>
<style scoped>
.graph-content {
padding: 0.5rem;
}
.graph-controls {
margin-bottom: 1rem;
}
.graph-controls label {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.graph-controls select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.graph-container {
width: 100%;
height: 220px;
margin-top: 1rem;
}
.graph-svg {
width: 100%;
height: 100%;
background: #fdf1db;
border-radius: 4px;
}
.axis-label {
font-size: 2.5px;
fill: #333;
}
.y-label {
dominant-baseline: middle;
}
.x-label {
dominant-baseline: hanging;
}
.graph-loading,
.graph-no-data {
margin: 1rem 0;
text-align: center;
color: #666;
}
</style>