268 lines
7.3 KiB
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>
|