mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 17:24:26 +08:00
add transaction statistics page
This commit is contained in:
Generated
+74
@@ -16,6 +16,7 @@
|
|||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dom7": "^4.0.6",
|
"dom7": "^4.0.6",
|
||||||
|
"echarts": "^5.4.2",
|
||||||
"framework7": "^8.1.0",
|
"framework7": "^8.1.0",
|
||||||
"framework7-icons": "^5.0.5",
|
"framework7-icons": "^5.0.5",
|
||||||
"framework7-vue": "^8.1.0",
|
"framework7-vue": "^8.1.0",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"swiper": "^9.3.2",
|
"swiper": "^9.3.2",
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-echarts": "^6.6.0",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.2",
|
||||||
"vue3-perfect-scrollbar": "^1.6.1",
|
"vue3-perfect-scrollbar": "^1.6.1",
|
||||||
@@ -4177,6 +4179,15 @@
|
|||||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "5.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz",
|
||||||
|
"integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "5.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ejs": {
|
"node_modules/ejs": {
|
||||||
"version": "3.1.9",
|
"version": "3.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
||||||
@@ -7376,6 +7387,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resize-detector": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ=="
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.2",
|
"version": "1.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||||
@@ -7942,6 +7958,11 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -8244,6 +8265,51 @@
|
|||||||
"@vue/shared": "3.3.4"
|
"@vue/shared": "3.3.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-echarts": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-PpXDe1wKKzOJTLM8RM4GJxpRi+9eiC0YUJR7i44/AAa0oDatvNctJcZlfq/bUV4KV+HDsCeQzaB6JocrXEy9LA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"resize-detector": "^0.3.0",
|
||||||
|
"vue-demi": "^0.13.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.5",
|
||||||
|
"echarts": "^5.4.1",
|
||||||
|
"vue": "^2.6.12 || ^3.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-echarts/node_modules/vue-demi": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-eslint-parser": {
|
"node_modules/vue-eslint-parser": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz",
|
||||||
@@ -8811,6 +8877,14 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "5.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz",
|
||||||
|
"integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dom7": "^4.0.6",
|
"dom7": "^4.0.6",
|
||||||
|
"echarts": "^5.4.2",
|
||||||
"framework7": "^8.1.0",
|
"framework7": "^8.1.0",
|
||||||
"framework7-icons": "^5.0.5",
|
"framework7-icons": "^5.0.5",
|
||||||
"framework7-vue": "^8.1.0",
|
"framework7-vue": "^8.1.0",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"swiper": "^9.3.2",
|
"swiper": "^9.3.2",
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-echarts": "^6.6.0",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.2",
|
||||||
"vue3-perfect-scrollbar": "^1.6.1",
|
"vue3-perfect-scrollbar": "^1.6.1",
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog width="460" v-model="showState">
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar color="primary">
|
||||||
|
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<p v-if="hint">{{ hint }}</p>
|
||||||
|
<span v-if="beginDateTime && endDateTime">
|
||||||
|
<span>{{ beginDateTime }}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span>{{ endDateTime }}</span>
|
||||||
|
</span>
|
||||||
|
<slot></slot>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-text class="pa-4 w-100 d-flex justify-center">
|
||||||
|
<vue-date-picker range inline enable-seconds auto-apply
|
||||||
|
ref="datetimepicker"
|
||||||
|
month-name-format="long"
|
||||||
|
six-weeks="center"
|
||||||
|
:clearable="false"
|
||||||
|
:dark="isDarkMode"
|
||||||
|
:week-start="firstDayOfWeek"
|
||||||
|
:year-range="yearRange"
|
||||||
|
:day-names="dayNames"
|
||||||
|
:is24="is24Hour"
|
||||||
|
:partial-range="false"
|
||||||
|
:preset-ranges="presetRanges"
|
||||||
|
v-model="dateRange">
|
||||||
|
<template #month="{ text }">
|
||||||
|
{{ $t(`datetime.${text}.short`) }}
|
||||||
|
</template>
|
||||||
|
<template #month-overlay-value="{ text }">
|
||||||
|
{{ $t(`datetime.${text}.short`) }}
|
||||||
|
</template>
|
||||||
|
</vue-date-picker>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="gray" @click="cancel">{{ $t('Cancel') }}</v-btn>
|
||||||
|
<v-btn :disabled="!dateRange[0] || !dateRange[1]" @click="confirm">{{ $t('OK') }}</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
|
import { mapStores } from 'pinia';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
|
||||||
|
import datetimeConstants from '@/consts/datetime.js';
|
||||||
|
import { arrangeArrayWithNewStartIndex } from '@/lib/common.js';
|
||||||
|
import {
|
||||||
|
getCurrentUnixTime,
|
||||||
|
getCurrentDateTime,
|
||||||
|
getUnixTime,
|
||||||
|
getLocalDatetimeFromUnixTime,
|
||||||
|
getTodayFirstUnixTime,
|
||||||
|
getYear,
|
||||||
|
getDummyUnixTimeForLocalUsage,
|
||||||
|
getActualUnixTimeForStore,
|
||||||
|
getTimezoneOffsetMinutes,
|
||||||
|
getBrowserTimezoneOffsetMinutes,
|
||||||
|
getDateRangeByDateType
|
||||||
|
} from '@/lib/datetime.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'minTime',
|
||||||
|
'maxTime',
|
||||||
|
'title',
|
||||||
|
'hint',
|
||||||
|
'show'
|
||||||
|
],
|
||||||
|
emits: [
|
||||||
|
'update:show',
|
||||||
|
'dateRange:change'
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
const self = this;
|
||||||
|
let minDate = getTodayFirstUnixTime();
|
||||||
|
let maxDate = getCurrentUnixTime();
|
||||||
|
|
||||||
|
if (self.minTime) {
|
||||||
|
minDate = self.minTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.maxTime) {
|
||||||
|
maxDate = self.maxTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
yearRange: [
|
||||||
|
2000,
|
||||||
|
getYear(getCurrentDateTime()) + 1
|
||||||
|
],
|
||||||
|
dateRange: [
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(minDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())),
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(maxDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useUserStore),
|
||||||
|
showState: {
|
||||||
|
get: function () {
|
||||||
|
return this.show;
|
||||||
|
},
|
||||||
|
set: function (value) {
|
||||||
|
this.$emit('update:show', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDarkMode() {
|
||||||
|
return this.globalTheme.global.name.value === 'dark';
|
||||||
|
},
|
||||||
|
firstDayOfWeek() {
|
||||||
|
return this.userStore.currentUserFirstDayOfWeek;
|
||||||
|
},
|
||||||
|
dayNames() {
|
||||||
|
return arrangeArrayWithNewStartIndex(this.$locale.getAllMinWeekdayNames(), this.firstDayOfWeek);
|
||||||
|
},
|
||||||
|
is24Hour() {
|
||||||
|
return this.$locale.isLongTime24HourFormat(this.userStore);
|
||||||
|
},
|
||||||
|
beginDateTime() {
|
||||||
|
const actualBeginUnixTime = getActualUnixTimeForStore(getUnixTime(this.dateRange[0]), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, actualBeginUnixTime);
|
||||||
|
},
|
||||||
|
endDateTime() {
|
||||||
|
const actualEndUnixTime = getActualUnixTimeForStore(getUnixTime(this.dateRange[1]), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, actualEndUnixTime);
|
||||||
|
},
|
||||||
|
presetRanges() {
|
||||||
|
const presetRanges = [];
|
||||||
|
|
||||||
|
[
|
||||||
|
datetimeConstants.allDateRanges.Today,
|
||||||
|
datetimeConstants.allDateRanges.LastSevenDays,
|
||||||
|
datetimeConstants.allDateRanges.LastThirtyDays,
|
||||||
|
datetimeConstants.allDateRanges.ThisWeek,
|
||||||
|
datetimeConstants.allDateRanges.ThisMonth,
|
||||||
|
datetimeConstants.allDateRanges.ThisYear
|
||||||
|
].forEach(dateRangeType => {
|
||||||
|
const dateRange = getDateRangeByDateType(dateRangeType.type, this.firstDayOfWeek);
|
||||||
|
|
||||||
|
presetRanges.push({
|
||||||
|
label: this.$t(dateRangeType.name),
|
||||||
|
range: [
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.minTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())),
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.maxTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return presetRanges;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return {
|
||||||
|
globalTheme: theme
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
confirm() {
|
||||||
|
if (!this.dateRange[0] || !this.dateRange[1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMinDate = this.dateRange[0];
|
||||||
|
const currentMaxDate = this.dateRange[1];
|
||||||
|
|
||||||
|
let minUnixTime = getUnixTime(currentMinDate);
|
||||||
|
let maxUnixTime = getUnixTime(currentMaxDate);
|
||||||
|
|
||||||
|
if (minUnixTime < 0 || maxUnixTime < 0) {
|
||||||
|
this.$toast('Date is too early');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
minUnixTime = getActualUnixTimeForStore(minUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
maxUnixTime = getActualUnixTimeForStore(maxUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
|
||||||
|
this.$emit('dateRange:change', minUnixTime, maxUnixTime);
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.$emit('update:show', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -27,6 +27,7 @@ export default {
|
|||||||
'color',
|
'color',
|
||||||
'defaultColor',
|
'defaultColor',
|
||||||
'additionalColorAttr',
|
'additionalColorAttr',
|
||||||
|
'size',
|
||||||
'hiddenStatus'
|
'hiddenStatus'
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
@@ -102,6 +103,10 @@ export default {
|
|||||||
ret[additionalColorAttr] = color;
|
ret[additionalColorAttr] = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.size) {
|
||||||
|
ret['font-size'] = this.size;
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
getCategoryIconStyle(color, defaultColor, additionalColorAttr) {
|
getCategoryIconStyle(color, defaultColor, additionalColorAttr) {
|
||||||
@@ -119,6 +124,10 @@ export default {
|
|||||||
ret[additionalColorAttr] = color;
|
ret[additionalColorAttr] = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.size) {
|
||||||
|
ret['font-size'] = this.size;
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
getDefaultIconStyle(color, defaultColor, additionalColorAttr) {
|
getDefaultIconStyle(color, defaultColor, additionalColorAttr) {
|
||||||
@@ -136,6 +145,10 @@ export default {
|
|||||||
ret[additionalColorAttr] = color;
|
ret[additionalColorAttr] = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.size) {
|
||||||
|
ret['font-size'] = this.size;
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<v-chart class="pie-chart-container" autoresize :option="chartOptions"
|
||||||
|
@click="clickItem" @legendselectchanged="onLegendSelectChanged" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
|
import { mapStores } from 'pinia';
|
||||||
|
import { useSettingsStore } from '@/stores/setting.js';
|
||||||
|
|
||||||
|
import colorConstants from '@/consts/color.js';
|
||||||
|
import statisticsConstants from '@/consts/statistics.js';
|
||||||
|
import { formatPercent } from '@/lib/common.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'items',
|
||||||
|
'idField',
|
||||||
|
'nameField',
|
||||||
|
'valueField',
|
||||||
|
'percentField',
|
||||||
|
'currencyField',
|
||||||
|
'colorField',
|
||||||
|
'hiddenField',
|
||||||
|
'minValidPercent',
|
||||||
|
'defaultCurrency',
|
||||||
|
'showValue',
|
||||||
|
'enableClickItem'
|
||||||
|
],
|
||||||
|
emits: [
|
||||||
|
'click'
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedLegends: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useSettingsStore),
|
||||||
|
isDarkMode() {
|
||||||
|
return this.globalTheme.global.name.value === 'dark';
|
||||||
|
},
|
||||||
|
itemsMap: function () {
|
||||||
|
const map = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < this.items.length; i++) {
|
||||||
|
const item = this.items[i];
|
||||||
|
let id = '';
|
||||||
|
|
||||||
|
if (this.idField && item[this.idField]) {
|
||||||
|
id = item[this.idField];
|
||||||
|
} else {
|
||||||
|
id = item[this.nameField];
|
||||||
|
}
|
||||||
|
|
||||||
|
map[id] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
},
|
||||||
|
validItems: function () {
|
||||||
|
let totalValidValue = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.items.length; i++) {
|
||||||
|
const item = this.items[i];
|
||||||
|
|
||||||
|
if (item[this.valueField] && item[this.valueField] > 0 && (!this.hiddenField || !item[this.hiddenField])) {
|
||||||
|
totalValidValue += item[this.valueField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validItems = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.items.length; i++) {
|
||||||
|
const item = this.items[i];
|
||||||
|
|
||||||
|
if (item[this.valueField] && item[this.valueField] > 0 &&
|
||||||
|
(!this.hiddenField || !item[this.hiddenField]) &&
|
||||||
|
(!this.minValidPercent || item[this.valueField] / totalValidValue > this.minValidPercent)) {
|
||||||
|
const finalItem = {
|
||||||
|
id: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField],
|
||||||
|
name: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField],
|
||||||
|
displayName: item[this.nameField],
|
||||||
|
value: item[this.valueField],
|
||||||
|
percent: (item[this.percentField] > 0 || item[this.percentField] === 0 || item[this.percentField] === '0') ? item[this.percentField] : (item[this.valueField] / totalValidValue * 100),
|
||||||
|
actualPercent: item[this.valueField] / totalValidValue,
|
||||||
|
currency: item[this.currencyField],
|
||||||
|
itemStyle: {
|
||||||
|
color: this.getColor(item[this.colorField] ? item[this.colorField] : statisticsConstants.defaultChartColors[validItems.length % statisticsConstants.defaultChartColors.length]),
|
||||||
|
},
|
||||||
|
selected: true,
|
||||||
|
sourceItem: item
|
||||||
|
};
|
||||||
|
|
||||||
|
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '<0.01');
|
||||||
|
finalItem.displayValue = this.getDisplayCurrency(finalItem.value, (finalItem.currency || this.defaultCurrency));
|
||||||
|
|
||||||
|
validItems.push(finalItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validItems;
|
||||||
|
},
|
||||||
|
hasUnselectedItem: function () {
|
||||||
|
for (let i = 0; i < this.validItems.length; i++) {
|
||||||
|
const item = this.validItems[i];
|
||||||
|
|
||||||
|
if (this.selectedLegends && !this.selectedLegends[item.id]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
currentFirstItemPercent: function () {
|
||||||
|
let totalValue = 0;
|
||||||
|
let firstValue = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.validItems.length; i++) {
|
||||||
|
const item = this.validItems[i];
|
||||||
|
|
||||||
|
if (this.selectedLegends && !this.selectedLegends[item.id]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstValue === null) {
|
||||||
|
firstValue = item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalValue += item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstValue && totalValue > 0) {
|
||||||
|
return firstValue / totalValue;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chartOptions: function () {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: self.isDarkMode ? '#333' : '#fff',
|
||||||
|
textStyle: {
|
||||||
|
color: self.isDarkMode ? '#eee' : '#333'
|
||||||
|
},
|
||||||
|
formatter: params => {
|
||||||
|
const name = params.data ? params.data.displayName : '';
|
||||||
|
const value = params.data ? params.data.displayValue : self.getDisplayCurrency(params.value);
|
||||||
|
let percent = params.data ? params.data.displayPercent : (params.percent + '%');
|
||||||
|
|
||||||
|
if (self.hasUnselectedItem) {
|
||||||
|
percent = params.percent + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
return `${name}<br/>${value} (${percent})`;
|
||||||
|
} else {
|
||||||
|
return `${value} (${percent})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'horizontal',
|
||||||
|
left: 'top',
|
||||||
|
data: self.validItems.map(item => item.name),
|
||||||
|
selected: self.selectedLegends,
|
||||||
|
textStyle: {
|
||||||
|
color: self.isDarkMode ? '#eee' : '#333'
|
||||||
|
},
|
||||||
|
formatter: id => {
|
||||||
|
return self.itemsMap[id] && self.nameField && self.itemsMap[id][self.nameField] ? self.itemsMap[id][self.nameField] : id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
data: self.validItems,
|
||||||
|
top: 50,
|
||||||
|
startAngle: -90 + self.currentFirstItemPercent / 2 * 360,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: self.isDarkMode ? '#eee' : '#333',
|
||||||
|
formatter: params => {
|
||||||
|
return params.data ? params.data.displayName : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
minWidth: 600
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'left'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
top: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return {
|
||||||
|
globalTheme: theme
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickItem: function (e) {
|
||||||
|
if (this.enableClickItem && e.componentType === 'series' && e.seriesType ==='pie' && e.data && e.data.sourceItem) {
|
||||||
|
this.$emit('click', e.data.sourceItem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLegendSelectChanged: function (e) {
|
||||||
|
this.selectedLegends = e.selected;
|
||||||
|
},
|
||||||
|
getColor: function (color) {
|
||||||
|
if (color && color !== colorConstants.defaultColor) {
|
||||||
|
color = '#' + color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
},
|
||||||
|
getDisplayCurrency(value, currencyCode) {
|
||||||
|
return this.$locale.getDisplayCurrency(value, currencyCode, {
|
||||||
|
currencyDisplayMode: this.settingsStore.appSettings.currencyDisplayMode,
|
||||||
|
enableThousandsSeparator: this.settingsStore.appSettings.thousandsSeparator
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pie-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.pie-chart-container {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,8 @@ import { VApp } from 'vuetify/components/VApp';
|
|||||||
import { VAvatar } from 'vuetify/components/VAvatar';
|
import { VAvatar } from 'vuetify/components/VAvatar';
|
||||||
import { VAutocomplete } from 'vuetify/components/VAutocomplete';
|
import { VAutocomplete } from 'vuetify/components/VAutocomplete';
|
||||||
import { VBtn } from 'vuetify/components/VBtn';
|
import { VBtn } from 'vuetify/components/VBtn';
|
||||||
|
import { VBtnGroup } from 'vuetify/components/VBtnGroup';
|
||||||
|
import { VBtnToggle } from 'vuetify/components/VBtnToggle';
|
||||||
import { VCard, VCardActions, VCardItem, VCardSubtitle, VCardText, VCardTitle } from 'vuetify/components/VCard';
|
import { VCard, VCardActions, VCardItem, VCardSubtitle, VCardText, VCardTitle } from 'vuetify/components/VCard';
|
||||||
import { VChip } from 'vuetify/components/VChip';
|
import { VChip } from 'vuetify/components/VChip';
|
||||||
import { VDialog } from 'vuetify/components/VDialog';
|
import { VDialog } from 'vuetify/components/VDialog';
|
||||||
@@ -41,6 +43,15 @@ import { VWindow, VWindowItem } from 'vuetify/components/VWindow';
|
|||||||
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg';
|
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg';
|
||||||
import 'vuetify/styles';
|
import 'vuetify/styles';
|
||||||
|
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import { PieChart } from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
} from 'echarts/components';
|
||||||
|
import VChart from 'vue-echarts';
|
||||||
|
|
||||||
import 'line-awesome/dist/line-awesome/css/line-awesome.css';
|
import 'line-awesome/dist/line-awesome/css/line-awesome.css';
|
||||||
|
|
||||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||||
@@ -66,6 +77,8 @@ import AmountInput from '@/components/desktop/AmountInput.vue';
|
|||||||
import StepsBar from '@/components/desktop/StepsBar.vue';
|
import StepsBar from '@/components/desktop/StepsBar.vue';
|
||||||
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||||
|
import PieChartComponent from '@/components/desktop/PieChart.vue';
|
||||||
|
import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDialog.vue';
|
||||||
import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue';
|
import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue';
|
||||||
|
|
||||||
import '@/styles/desktop/template/base/libs/vuetify/_index.scss';
|
import '@/styles/desktop/template/base/libs/vuetify/_index.scss';
|
||||||
@@ -88,6 +101,8 @@ const vuetify = createVuetify({
|
|||||||
VAvatar,
|
VAvatar,
|
||||||
VAutocomplete,
|
VAutocomplete,
|
||||||
VBtn,
|
VBtn,
|
||||||
|
VBtnGroup,
|
||||||
|
VBtnToggle,
|
||||||
VCard,
|
VCard,
|
||||||
VCardActions,
|
VCardActions,
|
||||||
VCardItem,
|
VCardItem,
|
||||||
@@ -338,11 +353,19 @@ const vuetify = createVuetify({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
echarts.use([
|
||||||
|
CanvasRenderer,
|
||||||
|
PieChart,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent
|
||||||
|
]);
|
||||||
|
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
app.use(vuetify);
|
app.use(vuetify);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
|
app.component('VChart', VChart);
|
||||||
app.component('PerfectScrollbar', PerfectScrollbar);
|
app.component('PerfectScrollbar', PerfectScrollbar);
|
||||||
app.component('VueDatePicker', VueDatePicker);
|
app.component('VueDatePicker', VueDatePicker);
|
||||||
|
|
||||||
@@ -353,6 +376,8 @@ app.component('AmountInput', AmountInput);
|
|||||||
app.component('StepsBar', StepsBar);
|
app.component('StepsBar', StepsBar);
|
||||||
app.component('ConfirmDialog', ConfirmDialog);
|
app.component('ConfirmDialog', ConfirmDialog);
|
||||||
app.component('SnackBar', SnackBar);
|
app.component('SnackBar', SnackBar);
|
||||||
|
app.component('PieChart', PieChartComponent);
|
||||||
|
app.component('DateRangeSelectionDialog', DateRangeSelectionDialog);
|
||||||
app.component('SwitchToMobileDialog', SwitchToMobileDialog);
|
app.component('SwitchToMobileDialog', SwitchToMobileDialog);
|
||||||
|
|
||||||
app.config.globalProperties.$version = getVersion();
|
app.config.globalProperties.$version = getVersion();
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ input[type=number] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** custom class **/
|
/** custom class **/
|
||||||
|
:root {
|
||||||
|
--default-icon-color: var(--v-theme-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .dark {
|
||||||
|
--default-icon-color: var(--v-theme-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
.pin-codes-input {
|
.pin-codes-input {
|
||||||
--ebk-pin-code-input-height: 56px;
|
--ebk-pin-code-input-height: 56px;
|
||||||
--ebk-pin-code-input-gap: 12px;
|
--ebk-pin-code-input-gap: 12px;
|
||||||
@@ -83,3 +91,48 @@ input[type=number] {
|
|||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Replacing the default style of @vuepic/vue-datepicker **/
|
||||||
|
.dp__theme_light {
|
||||||
|
--dp-primary-color: #c67e48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__theme_dark {
|
||||||
|
--dp-primary-color: #c67e48;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fix @vuepic/vue-datepicker style issue **/
|
||||||
|
.dp__main.dp__flex_display {
|
||||||
|
flex-direction: column
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main .dp__preset_range {
|
||||||
|
white-space: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main .dp__menu_inner {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main .dp__menu_inner .dp__month_year_row > button {
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main .dp__menu_inner .dp__month_year_row > button.dp__button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main .dp__menu_inner .dp__month_year_row .dp__month_year_wrap > button {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main .dp__calendar .dp__calendar_item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main .dp__calendar .dp__calendar_item > .dp__cell_inner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,660 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-row class="match-height">
|
<v-row class="match-height">
|
||||||
statistics
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<div class="d-flex flex-column flex-md-row">
|
||||||
|
<div>
|
||||||
|
<v-tabs show-arrows direction="vertical"
|
||||||
|
class="text-uppercase my-4" v-model="query.chartDataType">
|
||||||
|
<v-tab :key="dataType.type" :value="dataType.type"
|
||||||
|
v-for="dataType in allChartDataTypes">
|
||||||
|
{{ $t(dataType.name) }}
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</div>
|
||||||
|
<v-window class="d-flex flex-grow-1 ml-md-5 disable-tab-transition statistics-container" v-model="activeTab">
|
||||||
|
<v-window-item value="statisticsPage">
|
||||||
|
<v-card variant="flat">
|
||||||
|
<template #title>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<div class="statistics-toolbar">
|
||||||
|
<v-btn-toggle
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
density="comfortable"
|
||||||
|
mandatory="force"
|
||||||
|
divided
|
||||||
|
:disabled="loading"
|
||||||
|
v-model="query.chartType"
|
||||||
|
>
|
||||||
|
<v-btn :value="allChartTypes.Pie" @click="setChartType(allChartTypes.Pie)">
|
||||||
|
{{ $t('Pie Chart') }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn :value="allChartTypes.Bar" @click="setChartType(allChartTypes.Bar)">
|
||||||
|
{{ $t('Bar Chart') }}
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
|
||||||
|
<v-btn-group class="ml-3" color="default"
|
||||||
|
density="comfortable" variant="outlined"
|
||||||
|
divided>
|
||||||
|
<v-btn :icon="icons.left"
|
||||||
|
:disabled="loading || query.dateType === allDateRanges.All.type || query.chartDataType === allChartDataTypes.AccountTotalAssets.type || query.chartDataType === allChartDataTypes.AccountTotalLiabilities.type"
|
||||||
|
@click="shiftDateRange(query.startTime, query.endTime, -1)"/>
|
||||||
|
<v-menu location="bottom">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn :disabled="loading || query.chartDataType === allChartDataTypes.AccountTotalAssets.type || query.chartDataType === allChartDataTypes.AccountTotalLiabilities.type"
|
||||||
|
v-bind="props">{{ dateRangeName(query) }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item :key="dateRange.type" :value="dateRange.type"
|
||||||
|
:append-icon="(query.dateType === dateRange.type ? icons.check : null)"
|
||||||
|
v-for="dateRange in allDateRanges">
|
||||||
|
<v-list-item-title
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="setDateFilter(dateRange.type)">
|
||||||
|
{{ $t(dateRange.name) }}
|
||||||
|
<div class="text-body-2" v-if="dateRange.type === allDateRanges.Custom.type && query.dateType === allDateRanges.Custom.type && query.startTime && query.endTime">
|
||||||
|
<small>
|
||||||
|
<span>{{ queryStartTime }}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<br/>
|
||||||
|
<span>{{ queryEndTime }}</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
<v-btn :icon="icons.right"
|
||||||
|
:disabled="loading || query.dateType === allDateRanges.All.type || query.chartDataType === allChartDataTypes.AccountTotalAssets.type || query.chartDataType === allChartDataTypes.AccountTotalLiabilities.type"
|
||||||
|
@click="shiftDateRange(query.startTime, query.endTime, 1)"/>
|
||||||
|
</v-btn-group>
|
||||||
|
|
||||||
|
<v-menu location="bottom">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn class="ml-3" color="default" variant="outlined"
|
||||||
|
:prepend-icon="icons.sort" :disabled="loading"
|
||||||
|
v-bind="props">{{ querySortingTypeName }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item :key="sortingType.type" :value="sortingType.type"
|
||||||
|
:append-icon="(query.sortingType === sortingType.type ? icons.check : null)"
|
||||||
|
v-for="sortingType in allSortingTypes">
|
||||||
|
<v-list-item-title
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="setSortingType(sortingType.type)">
|
||||||
|
{{ $t(sortingType.fullName) }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
<v-btn density="compact" color="default" variant="text"
|
||||||
|
class="ml-2" :icon="true" :disabled="loading"
|
||||||
|
v-if="!loading" @click="reload">
|
||||||
|
<v-icon :icon="icons.refresh" size="24" />
|
||||||
|
<v-tooltip activator="parent">{{ $t('Refresh') }}</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
<v-progress-circular indeterminate size="24" class="ml-2" v-if="loading"></v-progress-circular>
|
||||||
|
<v-spacer/>
|
||||||
|
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||||
|
:disabled="loading" :icon="true">
|
||||||
|
<v-icon :icon="icons.more" />
|
||||||
|
<v-menu activator="parent">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item :prepend-icon="icons.filter"
|
||||||
|
:title="$t('Filter Accounts')"
|
||||||
|
@click="filterAccounts"></v-list-item>
|
||||||
|
<v-list-item :prepend-icon="icons.filter"
|
||||||
|
:title="$t('Filter Transaction Categories')"
|
||||||
|
@click="filterCategories"></v-list-item>
|
||||||
|
<v-divider class="my-2"/>
|
||||||
|
<v-list-item :prepend-icon="icons.filterSettings"
|
||||||
|
:title="$t('Settings')"
|
||||||
|
@click="settings"></v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card-text v-if="initing">
|
||||||
|
<v-skeleton-loader type="paragraph" :loading="initing"
|
||||||
|
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]"></v-skeleton-loader>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-title class="statistics-overview-title pt-0" v-if="!initing">
|
||||||
|
<div>{{ totalAmountName }}</div>
|
||||||
|
<div class="statistics-overview-amount ml-3" :class="statisticsTextColor"
|
||||||
|
v-if="statisticsData && statisticsData.items && statisticsData.items.length">
|
||||||
|
{{ getDisplayAmount(statisticsData.totalAmount, defaultCurrency) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle-1 ml-3"
|
||||||
|
v-else-if="!statisticsData || !statisticsData.items || !statisticsData.items.length">
|
||||||
|
{{ $t('No transaction data') }}
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text v-if="!initing && query.chartType === allChartTypes.Pie">
|
||||||
|
<pie-chart
|
||||||
|
:items="statisticsData && statisticsData.items && statisticsData.items.length ? statisticsData.items : []"
|
||||||
|
:min-valid-percent="0.0001"
|
||||||
|
:show-value="showAmountInChart"
|
||||||
|
:enable-click-item="true"
|
||||||
|
:default-currency="defaultCurrency"
|
||||||
|
id-field="id"
|
||||||
|
name-field="name"
|
||||||
|
value-field="totalAmount"
|
||||||
|
percent-field="percent"
|
||||||
|
currency-field="currency"
|
||||||
|
hidden-field="hidden"
|
||||||
|
@click="clickPieChartItem"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-text v-if="!initing && query.chartType === allChartTypes.Bar">
|
||||||
|
<v-list rounded lines="two"
|
||||||
|
v-if="statisticsData && statisticsData.items && statisticsData.items.length">
|
||||||
|
<template :key="idx"
|
||||||
|
v-for="(item, idx) in statisticsData.items">
|
||||||
|
<v-list-item class="pl-0" v-if="!item.hidden">
|
||||||
|
<template #prepend>
|
||||||
|
<router-link class="statistics-list-item" :to="getItemLinkUrl(item)">
|
||||||
|
<ItemIcon :icon-type="queryChartDataCategory" size="34px"
|
||||||
|
:icon-id="item.icon"
|
||||||
|
:color="item.color"></ItemIcon>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<router-link class="statistics-list-item" :to="getItemLinkUrl(item)">
|
||||||
|
<div class="d-flex flex-column ml-2">
|
||||||
|
<div class="d-flex">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<small class="statistics-percent" v-if="item.percent >= 0">{{ getDisplayPercent(item.percent, 2, '<0.01') }}</small>
|
||||||
|
<v-spacer/>
|
||||||
|
<span class="statistics-amount">{{ getDisplayAmount(item.totalAmount, (item.currency || defaultCurrency)) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<v-progress-linear :color="item.color ? '#' + item.color : 'primary'"
|
||||||
|
:model-value="item.percent >= 0 ? item.percent : 0"
|
||||||
|
:height="4"></v-progress-linear>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</v-list-item>
|
||||||
|
<v-divider v-if="!item.hidden && idx !== statisticsData.items.length - 1"/>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<date-range-selection-dialog :title="$t('Custom Date Range')"
|
||||||
|
:min-time="query.startTime"
|
||||||
|
:max-time="query.endTime"
|
||||||
|
v-model:show="showCustomDateRangeDialog"
|
||||||
|
@dateRange:change="setCustomDateFilter" />
|
||||||
|
<snack-bar ref="snackbar" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
import { mapStores } from 'pinia';
|
||||||
created() {
|
import { useSettingsStore } from '@/stores/setting.js';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
import { useAccountsStore } from '@/stores/account.js';
|
||||||
|
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js';
|
||||||
|
import { useStatisticsStore } from '@/stores/statistics.js';
|
||||||
|
|
||||||
|
import datetimeConstants from '@/consts/datetime.js';
|
||||||
|
import statisticsConstants from '@/consts/statistics.js';
|
||||||
|
import { getNameByKeyValue, limitText, formatPercent } from '@/lib/common.js'
|
||||||
|
import {
|
||||||
|
parseDateFromUnixTime,
|
||||||
|
getYear,
|
||||||
|
getShiftedDateRange,
|
||||||
|
getDateRangeByDateType,
|
||||||
|
isDateRangeMatchFullYears,
|
||||||
|
isDateRangeMatchFullMonths
|
||||||
|
} from '@/lib/datetime.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mdiCheck,
|
||||||
|
mdiArrowLeft,
|
||||||
|
mdiArrowRight,
|
||||||
|
mdiSort,
|
||||||
|
mdiRefresh,
|
||||||
|
mdiFilterOutline,
|
||||||
|
mdiFilterCogOutline,
|
||||||
|
mdiPencilOutline,
|
||||||
|
mdiDotsVertical,
|
||||||
|
} from '@mdi/js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: 'statisticsPage',
|
||||||
|
initing: true,
|
||||||
|
loading: true,
|
||||||
|
showCustomDateRangeDialog: false,
|
||||||
|
icons: {
|
||||||
|
check: mdiCheck,
|
||||||
|
left: mdiArrowLeft,
|
||||||
|
right: mdiArrowRight,
|
||||||
|
sort: mdiSort,
|
||||||
|
refresh: mdiRefresh,
|
||||||
|
filter: mdiFilterOutline,
|
||||||
|
filterSettings: mdiFilterCogOutline,
|
||||||
|
pencil: mdiPencilOutline,
|
||||||
|
more: mdiDotsVertical
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useSettingsStore, useUserStore, useAccountsStore, useTransactionCategoriesStore, useStatisticsStore),
|
||||||
|
defaultCurrency() {
|
||||||
|
return this.userStore.currentUserDefaultCurrency;
|
||||||
|
},
|
||||||
|
firstDayOfWeek() {
|
||||||
|
return this.userStore.currentUserFirstDayOfWeek;
|
||||||
|
},
|
||||||
|
query() {
|
||||||
|
return this.statisticsStore.transactionStatisticsFilter;
|
||||||
|
},
|
||||||
|
queryChartDataCategory() {
|
||||||
|
return this.statisticsStore.transactionStatisticsChartDataCategory;
|
||||||
|
},
|
||||||
|
querySortingTypeName() {
|
||||||
|
const querySortingTypeName = getNameByKeyValue(this.allSortingTypes, this.query.sortingType, 'type', 'fullName', 'System Default');
|
||||||
|
return this.$t(querySortingTypeName);
|
||||||
|
},
|
||||||
|
queryStartTime() {
|
||||||
|
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, this.query.startTime);
|
||||||
|
},
|
||||||
|
queryEndTime() {
|
||||||
|
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, this.query.endTime);
|
||||||
|
},
|
||||||
|
allChartTypes() {
|
||||||
|
return statisticsConstants.allChartTypes;
|
||||||
|
},
|
||||||
|
allChartDataTypes() {
|
||||||
|
return statisticsConstants.allChartDataTypes;
|
||||||
|
},
|
||||||
|
allSortingTypes() {
|
||||||
|
return statisticsConstants.allSortingTypes;
|
||||||
|
},
|
||||||
|
allDateRanges() {
|
||||||
|
return datetimeConstants.allDateRanges;
|
||||||
|
},
|
||||||
|
showAccountBalance() {
|
||||||
|
return this.settingsStore.appSettings.showAccountBalance;
|
||||||
|
},
|
||||||
|
totalAmountName() {
|
||||||
|
if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type
|
||||||
|
|| this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type
|
||||||
|
|| this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type) {
|
||||||
|
return this.$t('Total Income');
|
||||||
|
} else if (this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type
|
||||||
|
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type
|
||||||
|
|| this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
|
||||||
|
return this.$t('Total Expense');
|
||||||
|
} else if (this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type) {
|
||||||
|
return this.$t('Total Assets');
|
||||||
|
} else if (this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type) {
|
||||||
|
return this.$t('Total Liabilities');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$t('Total Amount');
|
||||||
|
},
|
||||||
|
statisticsData() {
|
||||||
|
return this.statisticsStore.statisticsData;
|
||||||
|
},
|
||||||
|
statisticsTextColor() {
|
||||||
|
if (this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type ||
|
||||||
|
this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type ||
|
||||||
|
this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
|
||||||
|
return 'text-expense';
|
||||||
|
} else if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type ||
|
||||||
|
this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type ||
|
||||||
|
this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type) {
|
||||||
|
return 'text-income';
|
||||||
|
} else {
|
||||||
|
return 'text-default';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showAmountInChart() {
|
||||||
|
if (!this.showAccountBalance
|
||||||
|
&& (this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type || this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'query.chartDataType': function (newValue) {
|
||||||
|
this.statisticsStore.updateTransactionStatisticsFilter({
|
||||||
|
chartDataType: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
let defaultChartType = self.settingsStore.appSettings.statistics.defaultChartType;
|
||||||
|
|
||||||
|
if (defaultChartType !== self.allChartTypes.Pie && defaultChartType !== self.allChartTypes.Bar) {
|
||||||
|
defaultChartType = statisticsConstants.defaultChartType;
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultChartDataType = self.settingsStore.appSettings.statistics.defaultChartDataType;
|
||||||
|
|
||||||
|
if (defaultChartDataType < self.allChartDataTypes.ExpenseByAccount.type || defaultChartDataType > self.allChartDataTypes.AccountTotalLiabilities.type) {
|
||||||
|
defaultChartDataType = statisticsConstants.defaultChartDataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultDateRange = self.settingsStore.appSettings.statistics.defaultDataRangeType;
|
||||||
|
|
||||||
|
if (defaultDateRange < self.allDateRanges.All.type || defaultDateRange >= self.allDateRanges.Custom.type) {
|
||||||
|
defaultDateRange = statisticsConstants.defaultDataRangeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultSortType = self.settingsStore.appSettings.statistics.defaultSortingType;
|
||||||
|
|
||||||
|
if (defaultSortType < self.allSortingTypes.Amount.type || defaultSortType > self.allSortingTypes.Name.type) {
|
||||||
|
defaultSortType = statisticsConstants.defaultSortingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRange = getDateRangeByDateType(defaultDateRange, self.firstDayOfWeek);
|
||||||
|
|
||||||
|
self.statisticsStore.initTransactionStatisticsFilter({
|
||||||
|
dateType: dateRange ? dateRange.dateType : undefined,
|
||||||
|
startTime: dateRange ? dateRange.minTime : undefined,
|
||||||
|
endTime: dateRange ? dateRange.maxTime : undefined,
|
||||||
|
chartType: defaultChartType,
|
||||||
|
chartDataType: defaultChartDataType,
|
||||||
|
filterAccountIds: self.settingsStore.appSettings.statistics.defaultAccountFilter || {},
|
||||||
|
filterCategoryIds: self.settingsStore.appSettings.statistics.defaultTransactionCategoryFilter || {},
|
||||||
|
sortingType: defaultSortType,
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
self.accountsStore.loadAllAccounts({ force: false }),
|
||||||
|
self.transactionCategoriesStore.loadAllCategories({ force: false })
|
||||||
|
]).then(() => {
|
||||||
|
return self.statisticsStore.loadTransactionStatistics({
|
||||||
|
force: false
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
self.loading = false;
|
||||||
|
self.initing = false;
|
||||||
|
}).catch(error => {
|
||||||
|
self.loading = false;
|
||||||
|
self.initing = false;
|
||||||
|
|
||||||
|
if (!error.processed) {
|
||||||
|
self.$refs.snackbar.showError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reload(force) {
|
||||||
|
const self = this;
|
||||||
|
let dispatchPromise = null;
|
||||||
|
|
||||||
|
self.loading = true;
|
||||||
|
|
||||||
|
if (self.query.chartDataType === self.allChartDataTypes.ExpenseByAccount.type ||
|
||||||
|
self.query.chartDataType === self.allChartDataTypes.ExpenseByPrimaryCategory.type ||
|
||||||
|
self.query.chartDataType === self.allChartDataTypes.ExpenseBySecondaryCategory.type ||
|
||||||
|
self.query.chartDataType === self.allChartDataTypes.IncomeByAccount.type ||
|
||||||
|
self.query.chartDataType === self.allChartDataTypes.IncomeByPrimaryCategory.type ||
|
||||||
|
self.query.chartDataType === self.allChartDataTypes.IncomeBySecondaryCategory.type) {
|
||||||
|
dispatchPromise = self.statisticsStore.loadTransactionStatistics({
|
||||||
|
force: force
|
||||||
|
});
|
||||||
|
} else if (self.query.chartDataType === self.allChartDataTypes.AccountTotalAssets.type ||
|
||||||
|
self.query.chartDataType === self.allChartDataTypes.AccountTotalLiabilities.type) {
|
||||||
|
dispatchPromise = self.accountsStore.loadAllAccounts({
|
||||||
|
force: force
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dispatchPromise) {
|
||||||
|
dispatchPromise.then(() => {
|
||||||
|
self.loading = false;
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
self.$refs.snackbar.showMessage('Data has been updated');
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
self.loading = false;
|
||||||
|
|
||||||
|
if (!error.processed) {
|
||||||
|
self.$refs.snackbar.showError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setChartType(chartType) {
|
||||||
|
this.statisticsStore.updateTransactionStatisticsFilter({
|
||||||
|
chartType: chartType
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setSortingType(sortingType) {
|
||||||
|
if (sortingType < this.allSortingTypes.Amount.type || sortingType > this.allSortingTypes.Name.type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.statisticsStore.updateTransactionStatisticsFilter({
|
||||||
|
sortingType: sortingType
|
||||||
|
});
|
||||||
|
|
||||||
|
this.reload(null);
|
||||||
|
},
|
||||||
|
setDateFilter(dateType) {
|
||||||
|
if (dateType === this.allDateRanges.Custom.type) { // Custom
|
||||||
|
this.showCustomDateRangeDialog = true;
|
||||||
|
return;
|
||||||
|
} else if (this.query.dateType === dateType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRange = getDateRangeByDateType(dateType, this.firstDayOfWeek);
|
||||||
|
|
||||||
|
if (!dateRange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.statisticsStore.updateTransactionStatisticsFilter({
|
||||||
|
dateType: dateRange.dateType,
|
||||||
|
startTime: dateRange.minTime,
|
||||||
|
endTime: dateRange.maxTime
|
||||||
|
});
|
||||||
|
|
||||||
|
this.reload(null);
|
||||||
|
},
|
||||||
|
setCustomDateFilter(startTime, endTime) {
|
||||||
|
if (!startTime || !endTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.statisticsStore.updateTransactionStatisticsFilter({
|
||||||
|
dateType: this.allDateRanges.Custom.type,
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showCustomDateRangeDialog = false;
|
||||||
|
|
||||||
|
this.reload(null);
|
||||||
|
},
|
||||||
|
shiftDateRange(startTime, endTime, scale) {
|
||||||
|
if (this.query.dateType === this.allDateRanges.All.type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDateRange = getShiftedDateRange(startTime, endTime, scale);
|
||||||
|
let newDateType = this.allDateRanges.Custom.type;
|
||||||
|
|
||||||
|
for (let dateRangeField in this.allDateRanges) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(this.allDateRanges, dateRangeField)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRangeType = this.allDateRanges[dateRangeField];
|
||||||
|
const dateRange = getDateRangeByDateType(dateRangeType.type, this.firstDayOfWeek);
|
||||||
|
|
||||||
|
if (dateRange && dateRange.minTime === newDateRange.minTime && dateRange.maxTime === newDateRange.maxTime) {
|
||||||
|
newDateType = dateRangeType.type;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.statisticsStore.updateTransactionStatisticsFilter({
|
||||||
|
dateType: newDateType,
|
||||||
|
startTime: newDateRange.minTime,
|
||||||
|
endTime: newDateRange.maxTime
|
||||||
|
});
|
||||||
|
|
||||||
|
this.reload(null);
|
||||||
|
},
|
||||||
|
dateRangeName(query) {
|
||||||
|
if (query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type ||
|
||||||
|
query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type) {
|
||||||
|
return this.$t(this.allDateRanges.All.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.dateType === this.allDateRanges.All.type) {
|
||||||
|
return this.$t(this.allDateRanges.All.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let dateRangeField in this.allDateRanges) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(this.allDateRanges, dateRangeField)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRange = this.allDateRanges[dateRangeField];
|
||||||
|
|
||||||
|
if (dateRange && dateRange.type !== this.allDateRanges.Custom.type && dateRange.type === query.dateType && dateRange.name) {
|
||||||
|
return this.$t(dateRange.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDateRangeMatchFullYears(query.startTime, query.endTime)) {
|
||||||
|
const displayStartTime = this.$locale.formatUnixTimeToShortYear(this.userStore, query.startTime);
|
||||||
|
const displayEndTime = this.$locale.formatUnixTimeToShortYear(this.userStore, query.endTime);
|
||||||
|
|
||||||
|
return displayStartTime !== displayEndTime ? `${displayStartTime} ~ ${displayEndTime}` : displayStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDateRangeMatchFullMonths(query.startTime, query.endTime)) {
|
||||||
|
const displayStartTime = this.$locale.formatUnixTimeToShortYearMonth(this.userStore, query.startTime);
|
||||||
|
const displayEndTime = this.$locale.formatUnixTimeToShortYearMonth(this.userStore, query.endTime);
|
||||||
|
|
||||||
|
return displayStartTime !== displayEndTime ? `${displayStartTime} ~ ${displayEndTime}` : displayStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTimeYear = getYear(parseDateFromUnixTime(query.startTime));
|
||||||
|
const endTimeYear = getYear(parseDateFromUnixTime(query.endTime));
|
||||||
|
|
||||||
|
const displayStartTime = this.$locale.formatUnixTimeToShortDate(this.userStore, query.startTime);
|
||||||
|
const displayEndTime = this.$locale.formatUnixTimeToShortDate(this.userStore, query.endTime);
|
||||||
|
|
||||||
|
if (displayStartTime === displayEndTime) {
|
||||||
|
return displayStartTime;
|
||||||
|
} else if (startTimeYear === endTimeYear) {
|
||||||
|
const displayShortEndTime = this.$locale.formatUnixTimeToShortMonthDay(this.userStore, query.endTime);
|
||||||
|
return `${displayStartTime} ~ ${displayShortEndTime}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${displayStartTime} ~ ${displayEndTime}`;
|
||||||
|
},
|
||||||
|
clickPieChartItem(item) {
|
||||||
|
this.$router.push(this.getItemLinkUrl(item));
|
||||||
|
},
|
||||||
|
filterAccounts() {
|
||||||
|
|
||||||
|
},
|
||||||
|
filterCategories() {
|
||||||
|
|
||||||
|
},
|
||||||
|
settings() {
|
||||||
|
this.$router.push('/app/settings?tab=statisticsSetting');
|
||||||
|
},
|
||||||
|
getDisplayAmount(amount, currency, textLimit) {
|
||||||
|
amount = this.getDisplayCurrency(amount, currency);
|
||||||
|
|
||||||
|
if (!this.showAccountBalance
|
||||||
|
&& (this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type
|
||||||
|
|| this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type)
|
||||||
|
) {
|
||||||
|
return '***';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textLimit) {
|
||||||
|
return limitText(amount, textLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount;
|
||||||
|
},
|
||||||
|
getDisplayCurrency(value, currencyCode) {
|
||||||
|
return this.$locale.getDisplayCurrency(value, currencyCode, {
|
||||||
|
currencyDisplayMode: this.settingsStore.appSettings.currencyDisplayMode,
|
||||||
|
enableThousandsSeparator: this.settingsStore.appSettings.thousandsSeparator
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getDisplayPercent(value, precision, lowPrecisionValue) {
|
||||||
|
return formatPercent(value, precision, lowPrecisionValue);
|
||||||
|
},
|
||||||
|
getItemLinkUrl(item) {
|
||||||
|
return `/transactions?${this.statisticsStore.getTransactionListPageParams(item)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.statistics-container.v-window > .v-window__container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-toolbar {
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-overview-title {
|
||||||
|
line-height: 2rem !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-overview-amount {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-list-item {
|
||||||
|
color: var(--v-theme-on-default);
|
||||||
|
font-size: 1rem !important;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-list-item .statistics-percent {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-list-item .statistics-amount {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -170,6 +170,18 @@
|
|||||||
"url": "https://github.com/nolimits4web/skeleton-elements",
|
"url": "https://github.com/nolimits4web/skeleton-elements",
|
||||||
"licenseUrl": "https://github.com/nolimits4web/skeleton-elements/blob/v4.0.1/LICENSE"
|
"licenseUrl": "https://github.com/nolimits4web/skeleton-elements/blob/v4.0.1/LICENSE"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Apache ECharts",
|
||||||
|
"copyright": "Copyright © 2017-2023, The Apache Software Foundation Apache ECharts, ECharts, Apache, the Apache feather, and the Apache ECharts project logo are either registered trademarks or trademarks of the Apache Software Foundation.",
|
||||||
|
"url": "https://echarts.apache.org/",
|
||||||
|
"licenseUrl": "https://github.com/apache/echarts/blob/5.4.2/LICENSE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vue-echarts",
|
||||||
|
"copyright": "Copyright (c) 2016-present GU Yiling & ECOMFE",
|
||||||
|
"url": "https://github.com/ecomfe/vue-echarts",
|
||||||
|
"licenseUrl": "https://github.com/ecomfe/vue-echarts/blob/v6.6.0/LICENSE"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "vue3-perfect-scrollbar",
|
"name": "vue3-perfect-scrollbar",
|
||||||
"copyright": "Copyright (c) 2018 Adam",
|
"copyright": "Copyright (c) 2018 Adam",
|
||||||
|
|||||||
+1
-1
@@ -142,7 +142,7 @@ export default defineConfig(async () => {
|
|||||||
return 'moment';
|
return 'moment';
|
||||||
} else if (/[\\/]node_modules[\\/](dom7|framework7.*|skeleton-elements|swiper)[\\/]/i.test(id)) {
|
} else if (/[\\/]node_modules[\\/](dom7|framework7.*|skeleton-elements|swiper)[\\/]/i.test(id)) {
|
||||||
return 'vendor-mobile';
|
return 'vendor-mobile';
|
||||||
} else if (/[\\/]node_modules[\\/](vuetify|vue-router|vue3-perfect-scrollbar|@mdi.*)[\\/]/i.test(id)) {
|
} else if (/[\\/]node_modules[\\/](vuetify|vue-router|vue3-perfect-scrollbar|@mdi.*|echarts|vue-echarts)[\\/]/i.test(id)) {
|
||||||
return 'vendor-desktop';
|
return 'vendor-desktop';
|
||||||
} else if (/[\\/]node_modules[\\/]/i.test(id)) {
|
} else if (/[\\/]node_modules[\\/]/i.test(id)) {
|
||||||
return 'vendor-common';
|
return 'vendor-common';
|
||||||
|
|||||||
Reference in New Issue
Block a user