diff --git a/package-lock.json b/package-lock.json
index b80251c8..efab47c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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=="
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index c1d31439..11b904c1 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/consts/statistics.js b/src/consts/statistics.js
new file mode 100644
index 00000000..36fe2a9b
--- /dev/null
+++ b/src/consts/statistics.js
@@ -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,
+};
diff --git a/src/lib/services.js b/src/lib/services.js
index 3fa8ca63..4063860c 100644
--- a/src/lib/services.js
+++ b/src/lib/services.js
@@ -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);
},
diff --git a/src/mobile-main.js b/src/mobile-main.js
index 6c8b4f80..aea34706 100644
--- a/src/mobile-main.js
+++ b/src/mobile-main.js
@@ -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;
diff --git a/src/router/mobile.js b/src/router/mobile.js
index 3e9d177e..50c372c1 100644
--- a/src/router/mobile.js
+++ b/src/router/mobile.js
@@ -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
},
{
diff --git a/src/store/index.js b/src/store/index.js
index 83646005..d7045660 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -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,
diff --git a/src/store/mutations.js b/src/store/mutations.js
index c90938fb..ccb100a0 100644
--- a/src/store/mutations.js
+++ b/src/store/mutations.js
@@ -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';
diff --git a/src/store/statistics.js b/src/store/statistics.js
new file mode 100644
index 00000000..c1b70dab
--- /dev/null
+++ b/src/store/statistics.js
@@ -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);
+}
diff --git a/src/store/transaction.js b/src/store/transaction.js
index 4155b7c1..f1a12182 100644
--- a/src/store/transaction.js
+++ b/src/store/transaction.js
@@ -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);
diff --git a/src/store/user.js b/src/store/user.js
index 8d864de2..e059c738 100644
--- a/src/store/user.js
+++ b/src/store/user.js
@@ -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);
diff --git a/src/views/mobile/Home.vue b/src/views/mobile/Home.vue
index 5217f08e..7e5e747b 100644
--- a/src/views/mobile/Home.vue
+++ b/src/views/mobile/Home.vue
@@ -155,7 +155,7 @@
-
+
{{ $t('Statistics') }}
diff --git a/src/views/mobile/statistics/Overview.vue b/src/views/mobile/statistics/Overview.vue
deleted file mode 100644
index 66c9d6e1..00000000
--- a/src/views/mobile/statistics/Overview.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/views/mobile/statistics/Transaction.vue b/src/views/mobile/statistics/Transaction.vue
new file mode 100644
index 00000000..9c45b134
--- /dev/null
+++ b/src/views/mobile/statistics/Transaction.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+