diff --git a/src/components/desktop/MonthRangeSelectionDialog.vue b/src/components/desktop/MonthRangeSelectionDialog.vue new file mode 100644 index 00000000..96425f28 --- /dev/null +++ b/src/components/desktop/MonthRangeSelectionDialog.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/src/consts/datetime.js b/src/consts/datetime.js index a52d19bd..37903e96 100644 --- a/src/consts/datetime.js +++ b/src/consts/datetime.js @@ -240,14 +240,16 @@ const allDateRanges = { type: 9, name: 'This year', availableScenes: { - [allDateRangeScenes.Normal]: true + [allDateRangeScenes.Normal]: true, + [allDateRangeScenes.TrendAnalysis]: true } }, LastYear: { type: 10, name: 'Last year', availableScenes: { - [allDateRangeScenes.Normal]: true + [allDateRangeScenes.Normal]: true, + [allDateRangeScenes.TrendAnalysis]: true } }, RecentTwelveMonths: { diff --git a/src/desktop-main.js b/src/desktop-main.js index 43d6ac6c..3a8a2a45 100644 --- a/src/desktop-main.js +++ b/src/desktop-main.js @@ -93,6 +93,7 @@ import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue'; import SnackBar from '@/components/desktop/SnackBar.vue'; import PieChartComponent from '@/components/desktop/PieChart.vue'; import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDialog.vue'; +import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue'; import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue'; import '@/styles/desktop/template/vuetify/index.scss'; @@ -453,6 +454,7 @@ app.component('ConfirmDialog', ConfirmDialog); app.component('SnackBar', SnackBar); app.component('PieChart', PieChartComponent); app.component('DateRangeSelectionDialog', DateRangeSelectionDialog); +app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog); app.component('SwitchToMobileDialog', SwitchToMobileDialog); app.config.globalProperties.$version = getVersion(); diff --git a/src/lib/common.js b/src/lib/common.js index a92e7775..d2457673 100644 --- a/src/lib/common.js +++ b/src/lib/common.js @@ -37,7 +37,7 @@ export function isYearMonth(val) { return false; } - return isNumber(items[0]) && isNumber(items[1]); + return parseInt(items[0]) && parseInt(items[1]); } export function isEquals(obj1, obj2) { diff --git a/src/lib/datetime.js b/src/lib/datetime.js index b19b4db9..88bf64d6 100644 --- a/src/lib/datetime.js +++ b/src/lib/datetime.js @@ -1,7 +1,47 @@ import moment from 'moment'; import dateTimeConstants from '@/consts/datetime.js'; -import { isNumber } from './common.js'; +import { isObject, isString, isNumber } from './common.js'; + +export function isYearMonthValid(year, month) { + if (!isNumber(year) || !isNumber(month)) { + return false; + } + + return year > 0 && month >= 0 && month <= 11; +} + +export function getYearMonthObjectFromString(yearMonth) { + if (!isString(yearMonth)) { + return null; + } + + const items = yearMonth.split('-'); + + if (items.length !== 2) { + return null; + } + + const year = parseInt(items[0]); + const month = parseInt(items[1]) - 1; + + if (!isYearMonthValid(year, month)) { + return null; + } + + return { + year: year, + month: month + }; +} + +export function getYearMonthStringFromObject(yearMonth) { + if (!yearMonth || !isYearMonthValid(yearMonth.year, yearMonth.month)) { + return ''; + } + + return `${yearMonth.year}-${yearMonth.month + 1}`; +} export function getTwoDigitsString(value) { if (value < 10) { @@ -155,6 +195,14 @@ export function getYearAndMonth(date) { return `${year}-${month}`; } +export function getYearAndMonthFromUnixTime(unixTime) { + if (!unixTime) { + return ''; + } + + return getYearAndMonth(parseDateFromUnixTime(unixTime)); +} + export function getDay(date) { return moment(date).date(); } @@ -262,6 +310,24 @@ export function getSpecifiedDayFirstUnixTime(unixTime) { return moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).unix(); } +export function getYearMonthFirstUnixTime(yearMonth) { + if (isString(yearMonth)) { + yearMonth = getYearMonthObjectFromString(yearMonth); + } else if (isObject(yearMonth) && !isYearMonthValid(yearMonth.year, yearMonth.month)) { + yearMonth = null; + } + + if (!yearMonth) { + return 0; + } + + return moment().set({ year: yearMonth.year, month: yearMonth.month, date: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }).unix(); +} + +export function getYearMonthLastUnixTime(yearMonth) { + return moment.unix(getYearMonthFirstUnixTime(yearMonth)).add(1, 'months').subtract(1, 'seconds').unix(); +} + export function getDateTimeFormatType(allFormatMap, allFormatArray, localeDefaultFormatTypeName, systemDefaultFormatType, formatTypeValue) { if (formatTypeValue > dateTimeConstants.defaultDateTimeFormatValue && allFormatArray[formatTypeValue - 1] && allFormatArray[formatTypeValue - 1].key) { return allFormatArray[formatTypeValue - 1]; diff --git a/src/stores/statistics.js b/src/stores/statistics.js index edc8491c..e8be1cc3 100644 --- a/src/stores/statistics.js +++ b/src/stores/statistics.js @@ -20,6 +20,7 @@ import { isObject } from '@/lib/common.js'; import { + getYearAndMonthFromUnixTime, getDateRangeByDateType } from '@/lib/datetime.js'; @@ -39,6 +40,7 @@ export const useStatisticsStore = defineStore('statistics', { filterCategoryIds: {} }, transactionCategoryStatisticsData: {}, + transactionCategoryTrendsData: {}, transactionStatisticsStateInvalid: true }), getters: { @@ -424,6 +426,7 @@ export const useStatisticsStore = defineStore('statistics', { this.transactionStatisticsFilter.filterAccountIds = {}; this.transactionStatisticsFilter.filterCategoryIds = {}; this.transactionCategoryStatisticsData = {}; + this.transactionCategoryTrendsData = {}; this.transactionStatisticsStateInvalid = true; }, initTransactionStatisticsFilter(filter) { @@ -478,8 +481,8 @@ export const useStatisticsStore = defineStore('statistics', { categoricalChartEndTime: categoricalChartDateRange ? categoricalChartDateRange.maxTime : undefined, trendChartType: defaultTrendChartType, trendChartDateType: trendChartDateRange ? trendChartDateRange.dateType : undefined, - trendChartStartYearMonth: trendChartDateRange ? trendChartDateRange.minTime : undefined, - trendChartEndYearMonth: trendChartDateRange ? trendChartDateRange.maxTime : undefined, + trendChartStartYearMonth: trendChartDateRange ? getYearAndMonthFromUnixTime(trendChartDateRange.minTime) : undefined, + trendChartEndYearMonth: trendChartDateRange ? getYearAndMonthFromUnixTime(trendChartDateRange.maxTime) : undefined, filterAccountIds: settingsStore.appSettings.statistics.defaultAccountFilter || {}, filterCategoryIds: settingsStore.appSettings.statistics.defaultTransactionCategoryFilter || {}, sortingType: defaultSortType, @@ -587,11 +590,11 @@ export const useStatisticsStore = defineStore('statistics', { this.transactionStatisticsFilter.trendChartDateType = filter.trendChartDateType; } - if (filter && isYearMonth(filter.trendChartStartYearMonth)) { + if (filter && (isYearMonth(filter.trendChartStartYearMonth) || filter.trendChartStartYearMonth === '')) { this.transactionStatisticsFilter.trendChartStartYearMonth = filter.trendChartStartYearMonth; } - if (filter && isYearMonth(filter.trendChartEndYearMonth)) { + if (filter && (isYearMonth(filter.trendChartEndYearMonth) || filter.trendChartEndYearMonth === '')) { this.transactionStatisticsFilter.trendChartEndYearMonth = filter.trendChartEndYearMonth; } @@ -686,8 +689,49 @@ export const useStatisticsStore = defineStore('statistics', { }); }); }, - loadTrendAnalysis() { - return Promise.resolve(true); + loadTrendAnalysis({ force }) { + const self = this; + const settingsStore = useSettingsStore(); + + return new Promise((resolve, reject) => { + resolve(true); + + services.getTransactionStatisticsTrends({ + startYearMonth: self.transactionStatisticsFilter.trendChartStartYearMonth, + endYearMonth: self.transactionStatisticsFilter.trendChartEndYearMonth, + useTransactionTimezone: settingsStore.appSettings.statistics.defaultTimezoneType + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve transaction statistics' }); + return; + } + + if (self.transactionStatisticsStateInvalid) { + self.updateTransactionStatisticsInvalidState(false); + } + + if (force && data.result && isEquals(self.transactionCategoryTrendsData, data.result)) { + reject({ message: 'Data is up to date' }); + return; + } + + self.transactionCategoryTrendsData = data.result; + + resolve(data.result); + }).catch(error => { + logger.error('failed to retrieve 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 retrieve transaction statistics' }); + } else { + reject(error); + } + }); + }); }, } }); diff --git a/src/views/desktop/statistics/TransactionPage.vue b/src/views/desktop/statistics/TransactionPage.vue index ba7902bf..25d71818 100644 --- a/src/views/desktop/statistics/TransactionPage.vue +++ b/src/views/desktop/statistics/TransactionPage.vue @@ -55,11 +55,10 @@ {{ $t('Statistics & Analysis') }} - + + :disabled="loading || !canShiftDateRange(query)" + @click="shiftDateRange(query, -1)"/>