add parts of statistics ui

This commit is contained in:
MaysWind
2021-01-21 00:55:05 +08:00
parent 1f829f0225
commit 6f4bacd544
14 changed files with 468 additions and 16 deletions
+47 -2
View File
@@ -4623,6 +4623,22 @@
"safer-buffer": "^2.1.0"
}
},
"echarts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.0.1.tgz",
"integrity": "sha512-JYn22Dolt2esY2jEzUsw1OxbobuW67oGjIoTjZO3rW89SWkfJ4kbrmC2OW9JjsBrD1rdkmaWBuZZ2HgmThyxJw==",
"requires": {
"tslib": "2.0.3",
"zrender": "5.0.3"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7058,8 +7074,7 @@
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash.defaultsdeep": {
"version": "4.6.1",
@@ -9346,6 +9361,11 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"resize-detector": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.1.10.tgz",
"integrity": "sha512-iLcXC8A6Fb0DfA+TRiywrK/0A22bFqkhntjMJMEzXDA4XkcEkfwpNbv7W8iewUiD0xYIaeiXOfiEehTqGKsUFw=="
},
"resolve": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
@@ -11081,6 +11101,16 @@
"clipboard": "^2.0.0"
}
},
"vue-echarts": {
"version": "5.0.0-beta.0",
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-5.0.0-beta.0.tgz",
"integrity": "sha512-QZFKGXDAYFQo+F20REpzcdLx79nsl4kOorJRpN+08aYq4YiIlmtWss1Lxadm7Fo+NYyWm8nnT+h4xHv3uqWIDQ==",
"requires": {
"core-js": "^3.4.4",
"lodash": "^4.17.15",
"resize-detector": "^0.1.10"
}
},
"vue-eslint-parser": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.1.1.tgz",
@@ -12144,6 +12174,21 @@
"dev": true
}
}
},
"zrender": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.0.3.tgz",
"integrity": "sha512-TVcN2IMdo7je3GEq/E4CER4AGBe/n50/izILdupppyHf/hVHuiXCRliqdu8+32Z1OmGg6RfKt5qQlkX+bOtU0g==",
"requires": {
"tslib": "2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
}
}
}
+2
View File
@@ -12,6 +12,7 @@
"cbor-js": "^0.1.0",
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
"echarts": "^5.0.1",
"framework7": "^5.7.14",
"framework7-icons": "^3.0.1",
"framework7-vue": "^5.7.14",
@@ -21,6 +22,7 @@
"ua-parser-js": "^0.7.22",
"vue": "^2.6.12",
"vue-clipboard2": "^0.3.1",
"vue-echarts": "^5.0.0-beta.0",
"vue-i18n": "^8.22.1",
"vue-i18n-filter": "^0.1.6",
"vue-moment": "^4.1.0",
+21
View File
@@ -0,0 +1,21 @@
const allChartTypes = {
Pie: 0,
Bar: 1
};
const defaultChartType = allChartTypes.Pie;
const allChartLegendTypes = {
Account: 0,
PrimaryCategory: 1,
SecondaryCategory: 1
};
const defaultChartLegendType = allChartLegendTypes.SecondaryCategory;
export default {
allChartTypes: allChartTypes,
defaultChartType: defaultChartType,
allChartLegendTypes: allChartLegendTypes,
defaultChartLegendType: defaultChartLegendType,
};
+13
View File
@@ -185,6 +185,19 @@ export default {
return axios.get('v1/overviews/transaction.json' + (queryParams.length ? '?query=' + queryParams.join('|') : ''));
},
getTransactionStatistics: ({ startTime, endTime }) => {
const queryParams = [];
if (startTime) {
queryParams.push(`start_time=${startTime}`);
}
if (endTime) {
queryParams.push(`end_time=${endTime}`);
}
return axios.get('v1/statistics/transaction.json' + (queryParams.length ? '?' + queryParams.join('&') : ''));
},
getAllAccounts: ({ visibleOnly }) => {
return axios.get('v1/accounts/list.json?visible_only=' + !!visibleOnly);
},
+11
View File
@@ -4,6 +4,7 @@ import VueI18n from 'vue-i18n';
import VueI18nFilter from 'vue-i18n-filter';
import Framework7 from 'framework7/framework7.esm.bundle.js';
import Framework7Vue from 'framework7-vue/framework7-vue.esm.bundle.js';
import ECharts from 'vue-echarts';
import PincodeInput from 'vue-pincode-input';
import VueMoment from 'vue-moment';
import VueClipboard from 'vue-clipboard2';
@@ -15,6 +16,13 @@ import 'framework7-icons';
import 'line-awesome/dist/line-awesome/css/line-awesome.css';
import 'echarts/lib/chart/line';
import 'echarts/lib/chart/pie';
import 'echarts/lib/chart/bar';
import 'echarts/lib/component/legend';
import 'echarts/lib/component/title';
import 'echarts/lib/component/tooltip';
import { getAllLanguages, getLanguage, getDefaultLanguage, getI18nOptions, getLocalizedError, getLocalizedErrorParameters } from './lib/i18n.js';
import api from './consts/api.js';
import datetime from './consts/datetime.js';
@@ -24,6 +32,7 @@ import icons from './consts/icon.js';
import account from './consts/account.js';
import transaction from './consts/transaction.js';
import category from './consts/category.js';
import statistics from './consts/statistics.js';
import licenses from './lib/licenses.js';
import version from './lib/version.js';
import logger from './lib/logger.js';
@@ -62,6 +71,7 @@ Vue.use(VueI18n);
Vue.use(VueI18nFilter);
Vue.use(VueMoment, { moment });
Vue.use(VueClipboard);
Vue.component('v-chart', ECharts);
Vue.component('PincodeInput', PincodeInput);
Vue.component('PasswordInputSheet', PasswordInputSheet);
Vue.component('PasscodeInputSheet', PasscodeInputSheet);
@@ -92,6 +102,7 @@ Vue.prototype.$constants = {
account: account,
transaction: transaction,
category: category,
statistics: statistics,
};
Vue.prototype.$utilities = utils;
+3 -3
View File
@@ -11,7 +11,7 @@ import TransactionEditPage from '../views/mobile/transactions/Edit.vue';
import AccountListPage from '../views/mobile/accounts/List.vue';
import AccountEditPage from '../views/mobile/accounts/Edit.vue';
import StatisticsOverviewPage from '../views/mobile/statistics/Overview.vue';
import StatisticsTransactionPage from '../views/mobile/statistics/Transaction.vue';
import SettingsPage from '../views/mobile/Settings.vue';
import ApplicationLockPage from '../views/mobile/ApplicationLock.vue';
@@ -171,8 +171,8 @@ const routes = [
beforeEnter: checkLogin
},
{
path: '/statistic/overview',
component: StatisticsOverviewPage,
path: '/statistic/transaction',
component: StatisticsTransactionPage,
beforeEnter: checkLogin
},
{
+114
View File
@@ -1,4 +1,5 @@
import datetimeConstants from "../consts/datetime.js";
import statisticsConstants from "../consts/statistics.js";
import userState from "../lib/userstate.js";
import utils from "../lib/utils.js";
@@ -44,6 +45,11 @@ import {
LOAD_TRANSACTION_OVERVIEW,
UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE,
LOAD_TRANSACTION_STATISTICS,
INIT_TRANSACTION_STATISTICS_FILTER,
UPDATE_TRANSACTION_STATISTICS_FILTER,
UPDATE_TRANSACTION_STATISTICS_INVALID_STATE,
} from './mutations.js';
import {
@@ -88,6 +94,12 @@ import {
loadTransactionOverview
} from './overview.js';
import {
loadTransactionStatistics,
initTransactionStatisticsFilter,
updateTransactionStatisticsFilter
} from './statistics.js';
import {
loadAllAccounts,
getAccount,
@@ -165,6 +177,15 @@ const stores = {
transactionTagListStateInvalid: true,
transactionOverview: {},
transactionOverviewStateInvalid: true,
transactionStatisticsFilter: {
dateType: datetimeConstants.allDateRanges.ThisMonth.type,
startTime: 0,
endTime: 0,
chartType: statisticsConstants.defaultChartType,
chartLegendType: statisticsConstants.defaultChartLegendType,
},
transactionStatistics: [],
transactionStatisticsStateInvalid: true,
},
getters: {
// user
@@ -216,6 +237,14 @@ const stores = {
state.transactionOverview = {};
state.transactionOverviewStateInvalid = true;
state.transactionStatisticsFilter.dateType = datetimeConstants.allDateRanges.ThisMonth.type;
state.transactionStatisticsFilter.startTime = 0;
state.transactionStatisticsFilter.endTime = 0;
state.transactionStatisticsFilter.chartType = statisticsConstants.defaultChartType;
state.transactionStatisticsFilter.chartLegendType = statisticsConstants.defaultChartLegendType;
state.transactionStatistics = {};
state.transactionStatisticsStateInvalid = true;
clearExchangeRatesFromLocalStorage();
},
[STORE_USER_INFO] (state, userInfo) {
@@ -731,6 +760,86 @@ const stores = {
[UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE] (state, invalidState) {
state.transactionOverviewStateInvalid = invalidState;
},
[LOAD_TRANSACTION_STATISTICS] (state, { statistics, defaultCurrency }) {
if (statistics && statistics.items && statistics.items.length) {
for (let i = 0; i < statistics.items.length; i++) {
const item = statistics.items[i];
if (item.accountId) {
item.account = state.allAccountsMap[item.accountId];
}
if (item.categoryId) {
item.category = state.allTransactionCategoriesMap[item.categoryId];
}
if (item.account && item.account.currency !== defaultCurrency) {
item.amountInDefaultCurrency = getExchangedAmount(state)(item.amount, item.account.currency, defaultCurrency);
} else if (item.account && item.account.currency === defaultCurrency) {
item.amountInDefaultCurrency = item.amount;
} else {
item.amountInDefaultCurrency = null;
}
}
}
state.transactionStatistics = statistics;
},
[INIT_TRANSACTION_STATISTICS_FILTER] (state, filter) {
if (filter && utils.isNumber(filter.dateType)) {
state.transactionStatisticsFilter.dateType = filter.dateType;
} else {
state.transactionStatisticsFilter.dateType = datetimeConstants.allDateRanges.ThisMonth.type;
}
if (filter && utils.isNumber(filter.startTime)) {
state.transactionStatisticsFilter.startTime = filter.startTime;
} else {
state.transactionStatisticsFilter.startTime = 0;
}
if (filter && utils.isNumber(filter.endTime)) {
state.transactionStatisticsFilter.endTime = filter.endTime;
} else {
state.transactionStatisticsFilter.endTime = 0;
}
if (filter && utils.isNumber(filter.chartType)) {
state.transactionStatisticsFilter.chartType = filter.chartType;
} else {
state.transactionStatisticsFilter.chartType = statisticsConstants.defaultChartType;
}
if (filter && utils.isNumber(filter.chartLegendType)) {
state.transactionStatisticsFilter.chartLegendType = filter.chartLegendType;
} else {
state.transactionStatisticsFilter.chartLegendType = statisticsConstants.defaultChartLegendType;
}
},
[UPDATE_TRANSACTION_STATISTICS_FILTER] (state, filter) {
if (filter && utils.isNumber(filter.dateType)) {
state.transactionStatisticsFilter.dateType = filter.dateType;
}
if (filter && utils.isNumber(filter.startTime)) {
state.transactionStatisticsFilter.startTime = filter.startTime;
}
if (filter && utils.isNumber(filter.endTime)) {
state.transactionStatisticsFilter.endTime = filter.endTime;
}
if (filter && utils.isNumber(filter.chartType)) {
state.transactionStatisticsFilter.chartType = filter.chartType;
}
if (filter && utils.isNumber(filter.chartLegendType)) {
state.transactionStatisticsFilter.chartLegendType = filter.chartLegendType;
}
},
[UPDATE_TRANSACTION_STATISTICS_INVALID_STATE] (state, invalidState) {
state.transactionStatisticsStateInvalid = invalidState;
},
},
actions: {
// user
@@ -763,6 +872,11 @@ const stores = {
// overview
loadTransactionOverview,
// statistics
loadTransactionStatistics,
initTransactionStatisticsFilter,
updateTransactionStatisticsFilter,
// account
loadAllAccounts,
saveAccount,
+5
View File
@@ -39,3 +39,8 @@ export const UPDATE_TRANSACTION_TAG_LIST_INVALID_STATE = 'UPDATE_TRANSACTION_TAG
export const LOAD_TRANSACTION_OVERVIEW = 'LOAD_TRANSACTION_OVERVIEW';
export const UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE = 'UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE';
export const LOAD_TRANSACTION_STATISTICS = 'LOAD_TRANSACTION_STATISTICS';
export const INIT_TRANSACTION_STATISTICS_FILTER = 'INIT_TRANSACTION_STATISTICS_FILTER';
export const UPDATE_TRANSACTION_STATISTICS_FILTER = 'UPDATE_TRANSACTION_STATISTICS_FILTER';
export const UPDATE_TRANSACTION_STATISTICS_INVALID_STATE = 'UPDATE_TRANSACTION_STATISTICS_INVALID_STATE';
+54
View File
@@ -0,0 +1,54 @@
import services from '../lib/services.js';
import logger from '../lib/logger.js';
import {
LOAD_TRANSACTION_STATISTICS,
INIT_TRANSACTION_STATISTICS_FILTER,
UPDATE_TRANSACTION_STATISTICS_FILTER,
UPDATE_TRANSACTION_STATISTICS_INVALID_STATE
} from "./mutations.js";
export function loadTransactionStatistics(context, { defaultCurrency }) {
return new Promise((resolve, reject) => {
services.getTransactionStatistics({
startTime: context.state.transactionStatisticsFilter.startTime,
endTime: context.state.transactionStatisticsFilter.endTime
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to get transaction statistics' });
return;
}
context.commit(LOAD_TRANSACTION_STATISTICS, {
statistics: data.result,
defaultCurrency: defaultCurrency
});
if (context.state.transactionStatisticsStateInvalid) {
context.commit(UPDATE_TRANSACTION_STATISTICS_INVALID_STATE, false);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to get transaction statistics', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to get transaction statistics' });
} else {
reject(error);
}
});
});
}
export function initTransactionStatisticsFilter(context, filter) {
context.commit(INIT_TRANSACTION_STATISTICS_FILTER, filter);
}
export function updateTransactionStatisticsFilter(context, filter) {
context.commit(UPDATE_TRANSACTION_STATISTICS_FILTER, filter);
}
+9
View File
@@ -15,6 +15,7 @@ import {
UPDATE_TRANSACTION_LIST_INVALID_STATE,
UPDATE_ACCOUNT_LIST_INVALID_STATE,
UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE,
UPDATE_TRANSACTION_STATISTICS_INVALID_STATE,
} from './mutations.js';
const emptyTransactionResult = {
@@ -177,6 +178,10 @@ export function saveTransaction(context, { transaction, defaultCurrency }) {
context.commit(UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE, true);
}
if (!context.state.transactionStatisticsStateInvalid) {
context.commit(UPDATE_TRANSACTION_STATISTICS_INVALID_STATE, true);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to save transaction', error);
@@ -230,6 +235,10 @@ export function deleteTransaction(context, { transaction, defaultCurrency, befor
context.commit(UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE, true);
}
if (!context.state.transactionStatisticsStateInvalid) {
context.commit(UPDATE_TRANSACTION_STATISTICS_INVALID_STATE, true);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to delete transaction', error);
+10 -1
View File
@@ -13,7 +13,8 @@ import {
UPDATE_ACCOUNT_LIST_INVALID_STATE,
UPDATE_TRANSACTION_CATEGORY_LIST_INVALID_STATE,
UPDATE_TRANSACTION_TAG_LIST_INVALID_STATE,
UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE
UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE,
UPDATE_TRANSACTION_STATISTICS_INVALID_STATE
} from './mutations.js';
export function authorize(context, { loginName, password }) {
@@ -258,6 +259,10 @@ export function updateUserProfile(context, { profile, currentPassword }) {
context.commit(UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE, true);
}
if (!context.state.transactionStatisticsStateInvalid) {
context.commit(UPDATE_TRANSACTION_STATISTICS_INVALID_STATE, true);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to save user profile', error);
@@ -301,6 +306,10 @@ export function clearUserData(context, { password }) {
context.commit(UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE, true);
}
if (!context.state.transactionStatisticsStateInvalid) {
context.commit(UPDATE_TRANSACTION_STATISTICS_INVALID_STATE, true);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to clear user data', error);
+1 -1
View File
@@ -155,7 +155,7 @@
<f7-link href="/transaction/add">
<f7-icon f7="plus_square" class="lab-tarbar-big-icon"></f7-icon>
</f7-link>
<f7-link href="/statistic/overview">
<f7-link href="/statistic/transaction">
<f7-icon f7="chart_pie"></f7-icon>
<span class="tabbar-label">{{ $t('Statistics') }}</span>
</f7-link>
-9
View File
@@ -1,9 +0,0 @@
<template>
<f7-page>
<f7-navbar :title="$t('Statistics')" :back-link="$t('Back')"></f7-navbar>
<f7-list media-list class="skeleton-text">
<f7-list-item title="Placeholder"></f7-list-item>
</f7-list>
</f7-page>
</template>
+178
View File
@@ -0,0 +1,178 @@
<template>
<f7-page>
<f7-navbar :title="$t('Statistics')" :back-link="$t('Back')"></f7-navbar>
<f7-card>
<f7-card-content class="no-safe-areas chart-container" :padding="false">
<v-chart :options="chartData" v-if="chartData" />
</f7-card-content>
</f7-card>
</f7-page>
</template>
<script>
export default {
data() {
return {
loading: true
};
},
computed: {
defaultCurrency() {
if (this.query.accountId && this.query.accountId !== '0') {
const account = this.allAccounts[this.query.accountId];
if (account && account.currency && account.currency !== this.$constants.currency.parentAccountCurrencyPlaceholder) {
return account.currency;
}
}
return this.$store.getters.currentUserDefaultCurrency || this.$t('default.currency');
},
query() {
return this.$store.state.transactionStatisticsFilter;
},
chartData() {
if (!this.$store.state.transactionStatistics ||
!this.$store.state.transactionStatistics.items ||
!this.$store.state.transactionStatistics.items.length) {
return null;
}
const combinedData = {};
const data = [];
for (let i = 0; i < this.$store.state.transactionStatistics.items.length; i++) {
const item = this.$store.state.transactionStatistics.items[i];
if (!item.account || !item.category) {
continue;
}
if (item.category.type !== this.$constants.category.allCategoryTypes.Expense) {
continue;
}
if (this.query.chartLegendType === this.$constants.statistics.allChartLegendTypes.Account) {
if (this.$utilities.isNumber(item.amountInDefaultCurrency)) {
let totalAmount = combinedData[item.account.name];
if (totalAmount) {
totalAmount += totalAmount = item.amountInDefaultCurrency;
} else {
totalAmount = item.amountInDefaultCurrency;
}
combinedData[item.account.name] = totalAmount;
}
} else if (this.query.chartLegendType === this.$constants.statistics.allChartLegendTypes.SecondaryCategory) {
if (this.$utilities.isNumber(item.amountInDefaultCurrency)) {
let totalAmount = combinedData[item.category.name];
if (totalAmount) {
totalAmount += totalAmount = item.amountInDefaultCurrency;
} else {
totalAmount = item.amountInDefaultCurrency;
}
combinedData[item.category.name] = totalAmount;
}
}
}
let chartType = 'pie';
if (this.query.chartType === this.$constants.statistics.allChartTypes.Bar) {
chartType = 'bar';
}
for (let legendName in combinedData) {
if (!Object.prototype.hasOwnProperty.call(combinedData, legendName)) {
continue;
}
data.push({
name: legendName,
value: combinedData[legendName] / 100
});
}
return {
series: [
{
type: chartType,
data: data,
label: {
position: 'inside'
},
animation: false,
}
]
};
}
},
created() {
const self = this;
const router = self.$f7router;
const dateParam = self.$utilities.getDateRangeByDateType(self.query.dateType);
if (dateParam.minTime !== self.query.startTime || dateParam.maxTime !== self.query.endTime) {
self.$store.dispatch('updateTransactionStatisticsFilter', {
startTime: dateParam.minTime,
endTime: dateParam.maxTime
});
}
Promise.all([
self.$store.dispatch('loadAllAccounts', { force: false }),
self.$store.dispatch('loadAllCategories', { force: false })
]).then(() => {
return self.$store.dispatch('loadTransactionStatistics', {
defaultCurrency: self.defaultCurrency
});
}).then(() => {
self.loading = false;
}).catch(error => {
self.loading = false;
if (!error.processed) {
self.$toast(error.message || error);
router.back();
}
});
},
methods: {
reload(done) {
const self = this;
self.$store.dispatch('loadTransactionStatistics', {
defaultCurrency: self.defaultCurrency
}).then(() => {
if (done) {
done();
}
}).catch(error => {
if (done) {
done();
}
if (!error.processed) {
self.$toast(error.message || error);
}
});
}
}
};
</script>
<style>
.chart-container {
height: 400px;
}
.chart-container .echarts {
width: 100%;
height: 100%;
}
</style>