diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 4cf577a6..799142fb 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -89,7 +89,7 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.WebContext) (any, *err } } - totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword) + totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionCountReq.TagFilterType, transactionCountReq.AmountFilter, transactionCountReq.Keyword) if err != nil { log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) @@ -160,7 +160,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs var totalCount int64 if transactionListReq.WithCount { - totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword) + totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword) if err != nil { log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) @@ -168,7 +168,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs } } - transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true) + transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true) if err != nil { log.Errorf(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error()) @@ -260,7 +260,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any, } } - transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword) + transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword) if err != nil { log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error()) diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index ecee815b..b33d53ab 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -66,6 +66,17 @@ func (s TransactionDbType) ToTransactionType() (TransactionType, error) { } } +// TransactionTagFilterType represents transaction tag filter type +type TransactionTagFilterType byte + +// Transaction tag filter types +const ( + TRANSACTION_TAG_FILTER_HAS_ANY TransactionTagFilterType = 0 + TRANSACTION_TAG_FILTER_HAS_ALL TransactionTagFilterType = 1 + TRANSACTION_TAG_FILTER_NOT_HAS_ANY TransactionTagFilterType = 2 + TRANSACTION_TAG_FILTER_NOT_HAS_ALL TransactionTagFilterType = 3 +) + // Transaction represents transaction data stored in database type Transaction struct { TransactionId int64 `xorm:"PK"` @@ -140,49 +151,52 @@ type TransactionImportRequest struct { // TransactionCountRequest represents transaction count request type TransactionCountRequest struct { - Type TransactionDbType `form:"type" binding:"min=0,max=4"` - CategoryIds string `form:"category_ids"` - AccountIds string `form:"account_ids"` - TagIds string `form:"tag_ids"` - AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` - Keyword string `form:"keyword"` - MaxTime int64 `form:"max_time" binding:"min=0"` - MinTime int64 `form:"min_time" binding:"min=0"` + Type TransactionDbType `form:"type" binding:"min=0,max=4"` + CategoryIds string `form:"category_ids"` + AccountIds string `form:"account_ids"` + TagIds string `form:"tag_ids"` + TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` + Keyword string `form:"keyword"` + MaxTime int64 `form:"max_time" binding:"min=0"` + MinTime int64 `form:"min_time" binding:"min=0"` } // TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request type TransactionListByMaxTimeRequest struct { - Type TransactionDbType `form:"type" binding:"min=0,max=4"` - CategoryIds string `form:"category_ids"` - AccountIds string `form:"account_ids"` - TagIds string `form:"tag_ids"` - AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` - Keyword string `form:"keyword"` - MaxTime int64 `form:"max_time" binding:"min=0"` - MinTime int64 `form:"min_time" binding:"min=0"` - Page int32 `form:"page" binding:"min=0"` - Count int32 `form:"count" binding:"required,min=1,max=50"` - WithCount bool `form:"with_count"` - WithPictures bool `form:"with_pictures"` - TrimAccount bool `form:"trim_account"` - TrimCategory bool `form:"trim_category"` - TrimTag bool `form:"trim_tag"` + Type TransactionDbType `form:"type" binding:"min=0,max=4"` + CategoryIds string `form:"category_ids"` + AccountIds string `form:"account_ids"` + TagIds string `form:"tag_ids"` + TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` + Keyword string `form:"keyword"` + MaxTime int64 `form:"max_time" binding:"min=0"` + MinTime int64 `form:"min_time" binding:"min=0"` + Page int32 `form:"page" binding:"min=0"` + Count int32 `form:"count" binding:"required,min=1,max=50"` + WithCount bool `form:"with_count"` + WithPictures bool `form:"with_pictures"` + TrimAccount bool `form:"trim_account"` + TrimCategory bool `form:"trim_category"` + TrimTag bool `form:"trim_tag"` } // TransactionListInMonthByPageRequest represents all parameters of transaction listing by month request type TransactionListInMonthByPageRequest struct { - Year int32 `form:"year" binding:"required,min=1"` - Month int32 `form:"month" binding:"required,min=1"` - Type TransactionDbType `form:"type" binding:"min=0,max=4"` - CategoryIds string `form:"category_ids"` - AccountIds string `form:"account_ids"` - TagIds string `form:"tag_ids"` - AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` - Keyword string `form:"keyword"` - WithPictures bool `form:"with_pictures"` - TrimAccount bool `form:"trim_account"` - TrimCategory bool `form:"trim_category"` - TrimTag bool `form:"trim_tag"` + Year int32 `form:"year" binding:"required,min=1"` + Month int32 `form:"month" binding:"required,min=1"` + Type TransactionDbType `form:"type" binding:"min=0,max=4"` + CategoryIds string `form:"category_ids"` + AccountIds string `form:"account_ids"` + TagIds string `form:"tag_ids"` + TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` + Keyword string `form:"keyword"` + WithPictures bool `form:"with_pictures"` + TrimAccount bool `form:"trim_account"` + TrimCategory bool `form:"trim_category"` + TrimTag bool `form:"trim_tag"` } // TransactionStatisticRequest represents all parameters of transaction statistic request diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 1650dbde..812ac05e 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -75,11 +75,11 @@ func (s *TransactionService) GetAllTransactions(c core.Context, uid int64, pageC // GetAllTransactionsByMaxTime returns all transactions before given time func (s *TransactionService) GetAllTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) { - return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, count, false, noDuplicated) + return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, count, false, noDuplicated) } // GetTransactionsByMaxTime returns transactions before given time -func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { +func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -103,14 +103,9 @@ func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, actualCount++ } - condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, noDuplicated) + condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, noDuplicated) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) - - if len(tagIds) > 0 { - sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds)) - } else if noTags { - sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime)) - } + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) err = sess.Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions) @@ -118,7 +113,7 @@ func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, } // GetTransactionsInMonthByPage returns all transactions in given year and month -func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string) ([]*models.Transaction, error) { +func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -131,14 +126,9 @@ func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid in var transactions []*models.Transaction - condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true) + condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) - - if len(tagIds) > 0 { - sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds)) - } else if noTags { - sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime)) - } + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) err = sess.OrderBy("transaction_time desc").Find(&transactions) @@ -181,23 +171,18 @@ func (s *TransactionService) GetTransactionByTransactionId(c core.Context, uid i // GetAllTransactionCount returns total count of transactions func (s *TransactionService) GetAllTransactionCount(c core.Context, uid int64) (int64, error) { - return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "") + return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "") } // GetTransactionCount returns count of transactions -func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string) (int64, error) { +func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string) (int64, error) { if uid <= 0 { return 0, errs.ErrUserIdInvalid } - condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true) + condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) - - if len(tagIds) > 0 { - sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds)) - } else if noTags { - sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime)) - } + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) return sess.Count(&models.Transaction{}) } @@ -1753,7 +1738,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction return err } -func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) { +func (s *TransactionService) buildTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) { condition := "uid=? AND deleted=?" conditionParams := make([]any, 0, 16) conditionParams = append(conditionParams, uid) @@ -1909,38 +1894,41 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact return condition, conditionParams } -func (s *TransactionService) getTransactionQueryByTagIdsCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, tagIds []int64) *builder.Builder { - if len(tagIds) > 0 { - condition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false}) - - if maxTransactionTime > 0 { - condition = condition.And(builder.Lte{"transaction_time": maxTransactionTime}) - } - - if minTransactionTime > 0 { - condition = condition.And(builder.Gte{"transaction_time": minTransactionTime}) - } - - condition = condition.And(builder.In("tag_id", tagIds)) - - return builder.Select("transaction_id").From("transaction_tag_index").Where(condition) - } - - return nil -} - -func (s *TransactionService) getTransactionQueryByAllTagIdsCondition(uid int64, maxTransactionTime int64, minTransactionTime int64) *builder.Builder { - condition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false}) +func (s *TransactionService) appendFilterTagIdsConditionToQuery(sess *xorm.Session, uid int64, maxTransactionTime int64, minTransactionTime int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType) *xorm.Session { + subQueryCondition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false}) if maxTransactionTime > 0 { - condition = condition.And(builder.Lte{"transaction_time": maxTransactionTime}) + subQueryCondition = subQueryCondition.And(builder.Lte{"transaction_time": maxTransactionTime}) } if minTransactionTime > 0 { - condition = condition.And(builder.Gte{"transaction_time": minTransactionTime}) + subQueryCondition = subQueryCondition.And(builder.Gte{"transaction_time": minTransactionTime}) } - return builder.Select("transaction_id").From("transaction_tag_index").Where(condition) + if noTags { + subQuery := builder.Select("transaction_id").From("transaction_tag_index").Where(subQueryCondition) + sess.NotIn("transaction_id", subQuery) + return sess + } + + if len(tagIds) < 1 { + return sess + } + + subQueryCondition = subQueryCondition.And(builder.In("tag_id", tagIds)) + subQuery := builder.Select("transaction_id").From("transaction_tag_index").Where(subQueryCondition) + + if tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ALL || tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ALL { + subQuery = subQuery.GroupBy("transaction_id").Having(fmt.Sprintf("COUNT(DISTINCT tag_id) >= %d", len(tagIds))) + } + + if tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ALL { + sess.In("transaction_id", subQuery) + } else if tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ALL { + sess.NotIn("transaction_id", subQuery) + } + + return sess } func (s *TransactionService) isAccountIdValid(transaction *models.Transaction) error { diff --git a/src/consts/transaction.js b/src/consts/transaction.js index 78b0b516..2161c87c 100644 --- a/src/consts/transaction.js +++ b/src/consts/transaction.js @@ -36,6 +36,27 @@ const allTransactionEditScopeTypes = { } }; +const allTransactionTagFilterTypes = { + HasAny: { + type: 0, + name: 'With Any Selected Tags' + }, + HasAll: { + type: 1, + name: 'With All Selected Tags' + }, + NotHasAny: { + type: 2, + name: 'Without Any Selected Tags' + }, + NotHasAll: { + type: 3, + name: 'Without All Selected Tags' + } +}; + +const defaultTransactionTagFilterType = allTransactionTagFilterTypes.HasAny; + const minAmountNumber = -99999999999; // -999999999.99 const maxAmountNumber = 99999999999; // 999999999.99 const maxPictureCount = 10; @@ -43,6 +64,8 @@ const maxPictureCount = 10; export default { allTransactionTypes: allTransactionTypes, allTransactionEditScopeTypes: allTransactionEditScopeTypes, + allTransactionTagFilterTypes: allTransactionTagFilterTypes, + defaultTransactionTagFilterType: defaultTransactionTagFilterType, minAmountNumber: minAmountNumber, maxAmountNumber: maxAmountNumber, maxPictureCount: maxPictureCount, diff --git a/src/lib/i18n.js b/src/lib/i18n.js index 38cdfcf4..8ee8a297 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -1212,6 +1212,25 @@ function getAllTransactionEditScopeTypes(translateFn) { return allEditScopeTypes; } +function getAllTransactionTagFilterTypes(translateFn) { + const allTagFilterTypes = []; + + for (const typeName in transactionConstants.allTransactionTagFilterTypes) { + if (!Object.prototype.hasOwnProperty.call(transactionConstants.allTransactionTagFilterTypes, typeName)) { + continue; + } + + const tagFilterType = transactionConstants.allTransactionTagFilterTypes[typeName]; + + allTagFilterTypes.push({ + type: tagFilterType.type, + displayName: translateFn(tagFilterType.name) + }); + } + + return allTagFilterTypes; +} + function getAllTransactionScheduledFrequencyTypes(translateFn) { const allScheduledFrequencyTypes = []; @@ -1756,6 +1775,7 @@ export function i18nFunctions(i18nGlobal) { getAllStatisticsSortingTypes: () => getAllStatisticsSortingTypes(i18nGlobal.t), getAllStatisticsDateAggregationTypes: () => getAllStatisticsDateAggregationTypes(i18nGlobal.t), getAllTransactionEditScopeTypes: () => getAllTransactionEditScopeTypes(i18nGlobal.t), + getAllTransactionTagFilterTypes: () => getAllTransactionTagFilterTypes(i18nGlobal.t), getAllTransactionScheduledFrequencyTypes: () => getAllTransactionScheduledFrequencyTypes(i18nGlobal.t), getAllTransactionDefaultCategories: (categoryType, locale) => getAllTransactionDefaultCategories(categoryType, locale, i18nGlobal.t), getAllDisplayExchangeRates: (settingsStore, exchangeRatesData) => getAllDisplayExchangeRates(settingsStore, exchangeRatesData, i18nGlobal.t), diff --git a/src/lib/services.js b/src/lib/services.js index c4e85bbc..9e67cded 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -299,15 +299,15 @@ export default { id }); }, - getTransactions: ({ maxTime, minTime, count, page, withCount, type, categoryIds, accountIds, tagIds, amountFilter, keyword }) => { + getTransactions: ({ maxTime, minTime, count, page, withCount, type, categoryIds, accountIds, tagIds, tagFilterType, amountFilter, keyword }) => { amountFilter = encodeURIComponent(amountFilter); keyword = encodeURIComponent(keyword); - return axios.get(`v1/transactions/list.json?max_time=${maxTime}&min_time=${minTime}&type=${type}&category_ids=${categoryIds}&account_ids=${accountIds}&tag_ids=${tagIds}&amount_filter=${amountFilter}&keyword=${keyword}&count=${count}&page=${page}&with_count=${withCount}&trim_account=true&trim_category=true&trim_tag=true`); + return axios.get(`v1/transactions/list.json?max_time=${maxTime}&min_time=${minTime}&type=${type}&category_ids=${categoryIds}&account_ids=${accountIds}&tag_ids=${tagIds}&tag_filter_type=${tagFilterType}&amount_filter=${amountFilter}&keyword=${keyword}&count=${count}&page=${page}&with_count=${withCount}&trim_account=true&trim_category=true&trim_tag=true`); }, - getAllTransactionsByMonth: ({ year, month, type, categoryIds, accountIds, tagIds, amountFilter, keyword }) => { + getAllTransactionsByMonth: ({ year, month, type, categoryIds, accountIds, tagIds, tagFilterType, amountFilter, keyword }) => { amountFilter = encodeURIComponent(amountFilter); 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}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); + 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 }) => { const queryParams = []; diff --git a/src/locales/en.json b/src/locales/en.json index f49d0225..3d5e1b77 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1177,6 +1177,7 @@ "balanceTime": "Balance Time", "startTime": "Start Time", "endTime": "End Time", + "tagFilterType": "Tag Filter Type", "amountFilter": "Amount Filter", "sourceAccountId": "Source Account ID", "destinationAccountId": "Destination Account ID", @@ -1515,6 +1516,10 @@ "Source Account": "Source Account", "Destination Account": "Destination Account", "Without Tags": "Without Tags", + "With Any Selected Tags": "With Any Selected Tags", + "With All Selected Tags": "With All Selected Tags", + "Without Any Selected Tags": "Without Any Selected Tags", + "Without All Selected Tags": "Without All Selected Tags", "Multiple Tags": "Multiple Tags", "Transaction Time": "Transaction Time", "Scheduled Transaction Frequency": "Scheduled Transaction Frequency", diff --git a/src/locales/vi.json b/src/locales/vi.json index 5c263c71..cd5619ed 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1177,6 +1177,7 @@ "balanceTime": "Thời gian số dư", "startTime": "Thời gian bắt đầu", "endTime": "Thời gian kết thúc", + "tagFilterType": "Tag Filter Type", "amountFilter": "Bộ lọc số tiền", "sourceAccountId": "ID tài khoản nguồn", "destinationAccountId": "ID tài khoản đích", @@ -1515,6 +1516,10 @@ "Source Account": "Tài khoản nguồn", "Destination Account": "Tài khoản đích", "Without Tags": "Không có thẻ", + "With Any Selected Tags": "With Any Selected Tags", + "With All Selected Tags": "With All Selected Tags", + "Without Any Selected Tags": "Without Any Selected Tags", + "Without All Selected Tags": "Without All Selected Tags", "Multiple Tags": "Nhiều thẻ", "Transaction Time": "Thời gian giao dịch", "Scheduled Transaction Frequency": "Tần suất giao dịch theo lịch trình", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 14b2c482..f69fef12 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1177,6 +1177,7 @@ "balanceTime": "余额时间", "startTime": "开始时间", "endTime": "结束时间", + "tagFilterType": "标签过滤类型", "amountFilter": "金额过滤", "sourceAccountId": "来源账户ID", "destinationAccountId": "目标账户ID", @@ -1515,6 +1516,10 @@ "Source Account": "来源账户", "Destination Account": "目标账户", "Without Tags": "没有标签", + "With Any Selected Tags": "包含任意选中的标签", + "With All Selected Tags": "包含全部选中的标签", + "Without Any Selected Tags": "不包含任意选中的标签", + "Without All Selected Tags": "不包含全部选中的标签", "Multiple Tags": "多个标签", "Transaction Time": "交易时间", "Scheduled Transaction Frequency": "定时交易周期", diff --git a/src/router/desktop.js b/src/router/desktop.js index eae74ba3..691d2eea 100644 --- a/src/router/desktop.js +++ b/src/router/desktop.js @@ -104,6 +104,7 @@ const router = createRouter({ initCategoryIds: route.query.categoryIds, initAccountIds: route.query.accountIds, initTagIds: route.query.tagIds, + initTagFilterType: route.query.tagFilterType, initAmountFilter: route.query.amountFilter, initKeyword: route.query.keyword }) diff --git a/src/stores/transaction.js b/src/stores/transaction.js index e13e96e6..2668784a 100644 --- a/src/stores/transaction.js +++ b/src/stores/transaction.js @@ -374,6 +374,7 @@ export const useTransactionsStore = defineStore('transactions', { categoryIds: '', accountIds: '', tagIds: '', + tagFilterType: transactionConstants.defaultTransactionTagFilterType.type, amountFilter: '', keyword: '' }, @@ -671,6 +672,7 @@ export const useTransactionsStore = defineStore('transactions', { this.transactionsFilter.categoryIds = ''; this.transactionsFilter.accountIds = ''; this.transactionsFilter.tagIds = ''; + this.transactionsFilter.tagFilterType = transactionConstants.defaultTransactionTagFilterType.type; this.transactionsFilter.amountFilter = ''; this.transactionsFilter.keyword = ''; this.transactions = []; @@ -725,6 +727,12 @@ export const useTransactionsStore = defineStore('transactions', { this.transactionsFilter.tagIds = ''; } + if (filter && isNumber(filter.tagFilterType)) { + this.transactionsFilter.tagFilterType = filter.tagFilterType; + } else { + this.transactionsFilter.tagFilterType = transactionConstants.defaultTransactionTagFilterType.type; + } + if (filter && isString(filter.amountFilter)) { this.transactionsFilter.amountFilter = filter.amountFilter; } else { @@ -775,6 +783,11 @@ export const useTransactionsStore = defineStore('transactions', { changed = true; } + if (filter && isNumber(filter.tagFilterType) && this.transactionsFilter.tagFilterType !== filter.tagFilterType) { + this.transactionsFilter.tagFilterType = filter.tagFilterType; + changed = true; + } + if (filter && isString(filter.amountFilter) && this.transactionsFilter.amountFilter !== filter.amountFilter) { this.transactionsFilter.amountFilter = filter.amountFilter; changed = true; @@ -806,6 +819,10 @@ export const useTransactionsStore = defineStore('transactions', { querys.push('tagIds=' + this.transactionsFilter.tagIds); } + if (this.transactionsFilter.tagFilterType) { + querys.push('tagFilterType=' + this.transactionsFilter.tagFilterType); + } + querys.push('dateType=' + this.transactionsFilter.dateType); if (this.transactionsFilter.dateType === datetimeConstants.allDateRanges.Custom.type) { @@ -846,6 +863,7 @@ export const useTransactionsStore = defineStore('transactions', { categoryIds: self.transactionsFilter.categoryIds, accountIds: self.transactionsFilter.accountIds, tagIds: self.transactionsFilter.tagIds, + tagFilterType: self.transactionsFilter.tagFilterType, amountFilter: self.transactionsFilter.amountFilter, keyword: self.transactionsFilter.keyword }).then(response => { @@ -922,6 +940,7 @@ export const useTransactionsStore = defineStore('transactions', { categoryIds: self.transactionsFilter.categoryIds, accountIds: self.transactionsFilter.accountIds, tagIds: self.transactionsFilter.tagIds, + tagFilterType: self.transactionsFilter.tagFilterType, amountFilter: self.transactionsFilter.amountFilter, keyword: self.transactionsFilter.keyword }).then(response => { diff --git a/src/views/desktop/transactions/ListPage.vue b/src/views/desktop/transactions/ListPage.vue index 61939635..e6b88ece 100644 --- a/src/views/desktop/transactions/ListPage.vue +++ b/src/views/desktop/transactions/ListPage.vue @@ -376,6 +376,25 @@ + + + + + + + + + {{ filterType.displayName }} + + + + + @@ -588,6 +607,10 @@ import { mdiPencilBoxOutline, mdiArrowLeft, mdiArrowRight, + mdiPlusBoxMultipleOutline, + mdiCheckboxMultipleOutline, + mdiMinusBoxMultipleOutline, + mdiCloseBoxMultipleOutline, mdiPound, mdiTextBoxOutline, mdiDotsVertical @@ -609,6 +632,7 @@ export default { 'initCategoryIds', 'initAccountIds', 'initTagIds', + 'initTagFilterType', 'initAmountFilter', 'initKeyword' ], @@ -649,6 +673,10 @@ export default { modifyBalance: mdiPencilBoxOutline, arrowLeft: mdiArrowLeft, arrowRight: mdiArrowRight, + withAnyTags: mdiPlusBoxMultipleOutline, + withAllTags: mdiCheckboxMultipleOutline, + withoutAnyTags: mdiMinusBoxMultipleOutline, + withoutAllTags: mdiCloseBoxMultipleOutline, tag: mdiPound, templates: mdiTextBoxOutline, more: mdiDotsVertical @@ -911,6 +939,26 @@ export default { allTransactionTypes() { return transactionConstants.allTransactionTypes; }, + allTransactionTagFilterTypes() { + const allTagFilterTypes = this.$locale.getAllTransactionTagFilterTypes(); + const allTagFilterTypesWithIcon = []; + const tagFilterIconMap = { + [transactionConstants.allTransactionTagFilterTypes.HasAny.type]: this.icons.withAnyTags, + [transactionConstants.allTransactionTagFilterTypes.HasAll.type]: this.icons.withAllTags, + [transactionConstants.allTransactionTagFilterTypes.NotHasAny.type]: this.icons.withoutAnyTags, + [transactionConstants.allTransactionTagFilterTypes.NotHasAll.type]: this.icons.withoutAllTags + }; + + for (let i = 0; i < allTagFilterTypes.length; i++) { + allTagFilterTypesWithIcon.push({ + type: allTagFilterTypes[i].type, + displayName: allTagFilterTypes[i].displayName, + icon: tagFilterIconMap[allTagFilterTypes[i].type] + }); + } + + return allTagFilterTypesWithIcon; + }, allAccounts() { return this.accountsStore.allAccountsMap; }, @@ -985,6 +1033,7 @@ export default { categoryIds: this.initCategoryIds, accountIds: this.initAccountIds, tagIds: this.initTagIds, + tagFilterType: this.initTagFilterType, amountFilter: this.initAmountFilter, keyword: this.initKeyword }); @@ -1015,6 +1064,7 @@ export default { categoryIds: to.query.categoryIds, accountIds: to.query.accountIds, tagIds: to.query.tagIds, + tagFilterType: to.query.tagFilterType, amountFilter: to.query.amountFilter, keyword: to.query.keyword }); @@ -1042,6 +1092,7 @@ export default { categoryIds: query.categoryIds, accountIds: query.accountIds, tagIds: query.tagIds, + tagFilterType: parseInt(query.tagFilterType) >= 0 ? parseInt(query.tagFilterType) : undefined, amountFilter: query.amountFilter || '', keyword: query.keyword || '' }); @@ -1256,6 +1307,22 @@ export default { this.$router.push(this.getFilterLinkUrl()); } }, + changeTagFilterType(filterType) { + if (this.query.tagFilterType === filterType) { + return; + } + + const changed = this.transactionsStore.updateTransactionListFilter({ + tagFilterType: filterType + }); + + if (changed) { + this.loading = true; + this.currentPageTransactions = []; + this.transactionsStore.clearTransactions(); + this.$router.push(this.getFilterLinkUrl()); + } + }, changeAmountFilter(filterType) { this.currentAmountFilterType = ''; this.amountMenuState = false; diff --git a/src/views/mobile/transactions/ListPage.vue b/src/views/mobile/transactions/ListPage.vue index 6970d776..ca63a384 100644 --- a/src/views/mobile/transactions/ListPage.vue +++ b/src/views/mobile/transactions/ListPage.vue @@ -470,6 +470,21 @@ + + + + + + + +