diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 799142fb..8142ea16 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -299,8 +299,20 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, return nil, errs.ErrClientTimezoneOffsetInvalid } + var allTagIds []int64 + noTags := statisticReq.TagIds == "none" + + if !noTags { + allTagIds, err = a.getTagIds(statisticReq.TagIds) + + if err != nil { + log.Warnf(c, "[transactions.TransactionStatisticsHandler] get transaction tag ids error, because %s", err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + } + uid := c.GetCurrentUid() - totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, utcOffset, statisticReq.UseTransactionTimezone) + totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, utcOffset, statisticReq.UseTransactionTimezone) if err != nil { log.Errorf(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error()) @@ -350,8 +362,20 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext) return nil, errs.Or(err, errs.ErrOperationFailed) } + var allTagIds []int64 + noTags := statisticTrendsReq.TagIds == "none" + + if !noTags { + allTagIds, err = a.getTagIds(statisticTrendsReq.TagIds) + + if err != nil { + log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] get transaction tag ids error, because %s", err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + } + uid := c.GetCurrentUid() - allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyIncomeAndExpense(c, uid, startYear, startMonth, endYear, endMonth, utcOffset, statisticTrendsReq.UseTransactionTimezone) + allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyIncomeAndExpense(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, utcOffset, statisticTrendsReq.UseTransactionTimezone) if err != nil { log.Errorf(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error()) diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index b33d53ab..28e15620 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -201,15 +201,19 @@ type TransactionListInMonthByPageRequest struct { // TransactionStatisticRequest represents all parameters of transaction statistic request type TransactionStatisticRequest struct { - StartTime int64 `form:"start_time" binding:"min=0"` - EndTime int64 `form:"end_time" binding:"min=0"` - UseTransactionTimezone bool `form:"use_transaction_timezone"` + StartTime int64 `form:"start_time" binding:"min=0"` + EndTime int64 `form:"end_time" binding:"min=0"` + TagIds string `form:"tag_ids"` + TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"` + UseTransactionTimezone bool `form:"use_transaction_timezone"` } // TransactionStatisticTrendsRequest represents all parameters of transaction statistic trends request type TransactionStatisticTrendsRequest struct { YearMonthRangeRequest - UseTransactionTimezone bool `form:"use_transaction_timezone"` + TagIds string `form:"tag_ids"` + TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"` + UseTransactionTimezone bool `form:"use_transaction_timezone"` } // TransactionAmountsRequest represents all parameters of transaction amounts request diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 812ac05e..8ea0584d 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -1288,7 +1288,7 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c core.Context, ui } // GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range -func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) { +func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -1336,7 +1336,10 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor finalConditionParams = append(finalConditionParams, maxTransactionTime) } - err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) + sess := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) + + err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) if err != nil { return nil, err @@ -1394,7 +1397,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor } // GetAccountsAndCategoriesMonthlyIncomeAndExpense returns the every accounts monthly income and expense amount by specific date range -func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) { +func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -1447,7 +1450,10 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c c finalConditionParams = append(finalConditionParams, maxTransactionTime) } - err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) + sess := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) + + err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) if err != nil { return nil, err diff --git a/src/lib/services.js b/src/lib/services.js index 9e67cded..ef513309 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -309,7 +309,7 @@ export default { keyword = encodeURIComponent(keyword); return axios.get(`v1/transactions/list/by_month.json?year=${year}&month=${month}&type=${type}&category_ids=${categoryIds}&account_ids=${accountIds}&tag_ids=${tagIds}&tag_filter_type=${tagFilterType}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); }, - getTransactionStatistics: ({ startTime, endTime, useTransactionTimezone }) => { + getTransactionStatistics: ({ startTime, endTime, useTransactionTimezone, tagIds, tagFilterType }) => { const queryParams = []; if (startTime) { @@ -320,9 +320,17 @@ export default { queryParams.push(`end_time=${endTime}`); } + if (tagIds) { + queryParams.push(`tag_ids=${tagIds}`); + } + + if (tagFilterType) { + queryParams.push(`tag_filter_type=${tagFilterType}`); + } + return axios.get(`v1/transactions/statistics.json?use_transaction_timezone=${useTransactionTimezone}` + (queryParams.length ? '&' + queryParams.join('&') : '')); }, - getTransactionStatisticsTrends: ({ startYearMonth, endYearMonth, useTransactionTimezone }) => { + getTransactionStatisticsTrends: ({ startYearMonth, endYearMonth, useTransactionTimezone, tagIds, tagFilterType }) => { const queryParams = []; if (startYearMonth) { @@ -333,6 +341,14 @@ export default { queryParams.push(`end_year_month=${endYearMonth}`); } + if (tagIds) { + queryParams.push(`tag_ids=${tagIds}`); + } + + if (tagFilterType) { + queryParams.push(`tag_filter_type=${tagFilterType}`); + } + return axios.get(`v1/transactions/statistics/trends.json?use_transaction_timezone=${useTransactionTimezone}` + (queryParams.length ? '&' + queryParams.join('&') : '')); }, getTransactionAmounts: ({ useTransactionTimezone, today, thisWeek, thisMonth, thisYear, lastMonth, monthBeforeLastMonth, monthBeforeLast2Months, monthBeforeLast3Months, monthBeforeLast4Months, monthBeforeLast5Months, monthBeforeLast6Months, monthBeforeLast7Months, monthBeforeLast8Months, monthBeforeLast9Months, monthBeforeLast10Months }) => { diff --git a/src/router/desktop.js b/src/router/desktop.js index 691d2eea..70d5cabe 100644 --- a/src/router/desktop.js +++ b/src/router/desktop.js @@ -122,6 +122,8 @@ const router = createRouter({ initEndTime: route.query.endTime, initFilterAccountIds: route.query.filterAccountIds, initFilterCategoryIds: route.query.filterCategoryIds, + initTagIds: route.query.tagIds, + initTagFilterType: route.query.tagFilterType, initSortingType: route.query.sortingType, initTrendDateAggregationType: route.query.trendDateAggregationType }) diff --git a/src/stores/statistics.js b/src/stores/statistics.js index 36d30279..93adcbf0 100644 --- a/src/stores/statistics.js +++ b/src/stores/statistics.js @@ -7,6 +7,7 @@ import { useTransactionCategoriesStore } from './transactionCategory.js'; import { useExchangeRatesStore } from './exchangeRates.js'; import datetimeConstants from '@/consts/datetime.js'; +import transactionConstants from '@/consts/transaction.js'; import statisticsConstants from '@/consts/statistics.js'; import categoryConstants from '@/consts/category.js'; import iconConstants from '@/consts/icon.js'; @@ -16,6 +17,7 @@ import logger from '@/lib/logger.js'; import { isEquals, isNumber, + isString, isObject, isInteger, isYearMonth, @@ -283,7 +285,9 @@ export const useStatisticsStore = defineStore('statistics', { trendChartStartYearMonth: '', trendChartEndYearMonth: '', filterAccountIds: {}, - filterCategoryIds: {} + filterCategoryIds: {}, + tagIds: '', + tagFilterType: transactionConstants.defaultTransactionTagFilterType.type }, transactionCategoryStatisticsData: {}, transactionCategoryTrendsData: {}, @@ -560,6 +564,8 @@ export const useStatisticsStore = defineStore('statistics', { this.transactionStatisticsFilter.trendChartEndYearMonth = ''; this.transactionStatisticsFilter.filterAccountIds = {}; this.transactionStatisticsFilter.filterCategoryIds = {}; + this.transactionStatisticsFilter.tagIds = ''; + this.transactionStatisticsFilter.tagFilterType = transactionConstants.defaultTransactionTagFilterType.type; this.transactionCategoryStatisticsData = {}; this.transactionCategoryTrendsData = {}; this.transactionStatisticsStateInvalid = true; @@ -679,6 +685,18 @@ export const useStatisticsStore = defineStore('statistics', { this.transactionStatisticsFilter.filterCategoryIds = settingsStore.appSettings.statistics.defaultTransactionCategoryFilter || {}; } + if (filter && isString(filter.tagIds)) { + this.transactionStatisticsFilter.tagIds = filter.tagIds; + } else { + this.transactionStatisticsFilter.tagIds = ''; + } + + if (filter && isInteger(filter.tagFilterType)) { + this.transactionStatisticsFilter.tagFilterType = filter.tagFilterType; + } else { + this.transactionStatisticsFilter.tagFilterType = transactionConstants.defaultTransactionTagFilterType.type; + } + if (filter && isInteger(filter.sortingType)) { this.transactionStatisticsFilter.sortingType = filter.sortingType; } else { @@ -747,6 +765,16 @@ export const useStatisticsStore = defineStore('statistics', { changed = true; } + if (filter && isString(filter.tagIds) && this.transactionStatisticsFilter.tagIds !== filter.tagIds) { + this.transactionStatisticsFilter.tagIds = filter.tagIds; + changed = true; + } + + if (filter && isInteger(filter.tagFilterType) && this.transactionStatisticsFilter.tagFilterType !== filter.tagFilterType) { + this.transactionStatisticsFilter.tagFilterType = filter.tagFilterType; + changed = true; + } + if (filter && isInteger(filter.sortingType) && this.transactionStatisticsFilter.sortingType !== filter.sortingType) { this.transactionStatisticsFilter.sortingType = filter.sortingType; changed = true; @@ -798,6 +826,14 @@ export const useStatisticsStore = defineStore('statistics', { } } + if (this.transactionStatisticsFilter.tagIds) { + querys.push('tagIds=' + this.transactionStatisticsFilter.tagIds); + } + + if (this.transactionStatisticsFilter.tagFilterType) { + querys.push('tagFilterType=' + this.transactionStatisticsFilter.tagFilterType); + } + querys.push('sortingType=' + this.transactionStatisticsFilter.sortingType); return querys.join('&'); @@ -847,6 +883,14 @@ export const useStatisticsStore = defineStore('statistics', { } } + if (this.transactionStatisticsFilter.tagIds) { + querys.push('tagIds=' + this.transactionStatisticsFilter.tagIds); + } + + if (this.transactionStatisticsFilter.tagFilterType) { + querys.push('tagFilterType=' + this.transactionStatisticsFilter.tagFilterType); + } + if (analysisType === statisticsConstants.allAnalysisTypes.CategoricalAnalysis && this.transactionStatisticsFilter.chartDataType !== statisticsConstants.allChartDataTypes.AccountTotalAssets.type && this.transactionStatisticsFilter.chartDataType !== statisticsConstants.allChartDataTypes.AccountTotalLiabilities.type) { @@ -872,6 +916,8 @@ export const useStatisticsStore = defineStore('statistics', { services.getTransactionStatistics({ startTime: self.transactionStatisticsFilter.categoricalChartStartTime, endTime: self.transactionStatisticsFilter.categoricalChartEndTime, + tagIds: self.transactionStatisticsFilter.tagIds, + tagFilterType: self.transactionStatisticsFilter.tagFilterType, useTransactionTimezone: settingsStore.appSettings.statistics.defaultTimezoneType }).then(response => { const data = response.data; @@ -914,6 +960,8 @@ export const useStatisticsStore = defineStore('statistics', { services.getTransactionStatisticsTrends({ startYearMonth: self.transactionStatisticsFilter.trendChartStartYearMonth, endYearMonth: self.transactionStatisticsFilter.trendChartEndYearMonth, + tagIds: self.transactionStatisticsFilter.tagIds, + tagFilterType: self.transactionStatisticsFilter.tagFilterType, useTransactionTimezone: settingsStore.appSettings.statistics.defaultTimezoneType }).then(response => { const data = response.data; diff --git a/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue b/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue index 2c4630a4..2744d4d2 100644 --- a/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue +++ b/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue @@ -76,6 +76,17 @@ +
+ + {{ filterType.displayName }} + +
+ @@ -123,6 +134,9 @@ import { mapStores } from 'pinia'; import { useTransactionTagsStore } from '@/stores/transactionTag.js'; import { useTransactionsStore } from '@/stores/transaction.js'; +import { useStatisticsStore } from '@/stores/statistics.js'; + +import transactionConstants from '@/consts/transaction.js'; import { selectAll, @@ -137,6 +151,7 @@ import { mdiEyeOutline, mdiEyeOffOutline, mdiDotsVertical, + mdiCheck, mdiPound } from '@mdi/js'; @@ -154,6 +169,7 @@ export default { loading: true, expandTagCategories: [ 'default' ], filterTagIds: {}, + tagFilterType: transactionConstants.defaultTransactionTagFilterType.type, showHidden: false, icons: { selectAll: mdiSelectAll, @@ -162,12 +178,13 @@ export default { show: mdiEyeOutline, hide: mdiEyeOffOutline, more: mdiDotsVertical, + check: mdiCheck, tag: mdiPound } } }, computed: { - ...mapStores(useTransactionTagsStore, useTransactionsStore), + ...mapStores(useTransactionTagsStore, useTransactionsStore, useStatisticsStore), title() { return 'Filter Transaction Tags'; }, @@ -177,6 +194,9 @@ export default { allTags() { return this.transactionTagsStore.allTransactionTags; }, + allTagFilterTypes() { + return this.$locale.getAllTransactionTagFilterTypes(); + }, hasAnyAvailableTag() { return this.transactionTagsStore.allAvailableTagsCount > 0; }, @@ -207,7 +227,20 @@ export default { allTransactionTagIds[transactionTag.id] = true; } - if (self.type === 'transactionListCurrent') { + if (self.type === 'statisticsCurrent') { + let transactionTagIds = self.statisticsStore.transactionStatisticsFilter.tagIds ? self.statisticsStore.transactionStatisticsFilter.tagIds.split(',') : []; + + for (let i = 0; i < transactionTagIds.length; i++) { + const transactionTagId = transactionTagIds[i]; + const transactionTag = self.transactionTagsStore.allTransactionTagsMap[transactionTagId]; + + if (transactionTag) { + allTransactionTagIds[transactionTag.id] = false; + } + } + self.filterTagIds = allTransactionTagIds; + self.tagFilterType = self.statisticsStore.transactionStatisticsFilter.tagFilterType; + } else if (self.type === 'transactionListCurrent') { for (let transactionTagId in self.transactionsStore.allFilterTagIds) { if (!Object.prototype.hasOwnProperty.call(self.transactionsStore.allFilterTagIds, transactionTagId)) { continue; @@ -257,7 +290,16 @@ export default { } } - if (this.type === 'transactionListCurrent') { + if (this.type === 'statisticsCurrent') { + changed = self.statisticsStore.updateTransactionStatisticsFilter({ + tagIds: finalTagIds, + tagFilterType: self.tagFilterType + }); + + if (changed) { + self.statisticsStore.updateTransactionStatisticsInvalidState(true); + } + } else if (this.type === 'transactionListCurrent') { changed = self.transactionsStore.updateTransactionListFilter({ tagIds: finalTagIds }); @@ -305,6 +347,17 @@ export default {