From 6430a52027bf6bbc5d8ae0f1da9a9e3824fc9c83 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 24 Nov 2025 02:12:44 +0800 Subject: [PATCH] tag filter supports selecting both included and excluded tags simultaneously --- cmd/webserver.go | 1 + pkg/api/data_managements.go | 10 +- pkg/api/transactions.go | 52 +++--- pkg/errs/global.go | 5 + pkg/mcp/query_transactions_tool_handler.go | 4 +- pkg/models/data_management.go | 17 +- pkg/models/transaction.go | 145 ++++++++++------ pkg/models/transaction_test.go | 95 ++++++++++ pkg/services/transactions.go | 97 ++++++----- pkg/utils/api.go | 2 + pkg/validators/tag_filter.go | 26 +++ pkg/validators/tag_filter_test.go | 104 +++++++++++ src/consts/api.ts | 8 + src/core/transaction.ts | 16 +- src/lib/services.ts | 27 ++- src/locales/de.json | 21 ++- src/locales/en.json | 21 ++- src/locales/es.json | 21 ++- src/locales/fr.json | 21 ++- src/locales/helpers.ts | 4 +- src/locales/it.json | 21 ++- src/locales/ja.json | 21 ++- src/locales/ko.json | 21 ++- src/locales/nl.json | 21 ++- src/locales/pt_BR.json | 21 ++- src/locales/ru.json | 21 ++- src/locales/th.json | 21 ++- src/locales/uk.json | 21 ++- src/locales/vi.json | 21 ++- src/locales/zh_Hans.json | 21 ++- src/locales/zh_Hant.json | 21 ++- src/models/data_management.ts | 3 +- src/models/transaction.ts | 84 ++++++++- src/router/desktop.ts | 6 +- src/stores/statistics.ts | 58 ++----- src/stores/transaction.ts | 77 +++++---- src/styles/desktop/global.scss | 27 +++ .../TransactionTagFilterSettingPageBase.ts | 119 +++++++------ .../transactions/TransactionListPageBase.ts | 16 +- .../TransactionTagFilterSettingsCard.vue | 162 ++++++++++-------- .../desktop/statistics/TransactionPage.vue | 9 +- src/views/desktop/transactions/ListPage.vue | 130 ++++---------- .../tabs/ImportTransactionDefineColumnTab.vue | 42 +---- .../TransactionTagFilterSettingsPage.vue | 117 ++++++++----- src/views/mobile/transactions/ListPage.vue | 79 +++------ 45 files changed, 1151 insertions(+), 706 deletions(-) create mode 100644 pkg/validators/tag_filter.go create mode 100644 pkg/validators/tag_filter_test.go diff --git a/cmd/webserver.go b/cmd/webserver.go index ccf40d25..173b73dc 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -116,6 +116,7 @@ func startWebServer(c *core.CliContext) error { _ = v.RegisterValidation("validCurrency", validators.ValidCurrency) _ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor) _ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter) + _ = v.RegisterValidation("validTagFilter", validators.ValidTagFilter) _ = v.RegisterValidation("validFiscalYearStart", validators.ValidateFiscalYearStart) } diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index 813aa040..6b1b2151 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -372,14 +372,14 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType return nil, "", errs.Or(err, errs.ErrOperationFailed) } - var allTagIds []int64 - noTags := exportTransactionDataReq.TagIds == "none" + noTags := exportTransactionDataReq.TagFilter == models.TransactionNoTagFilterValue + var tagFilters []*models.TransactionTagFilter if !noTags { - allTagIds, err = a.tags.GetTagIds(exportTransactionDataReq.TagIds) + tagFilters, err = models.ParseTransactionTagFilter(exportTransactionDataReq.TagFilter) if err != nil { - log.Warnf(c, "[data_managements.getExportedFileContent] get transaction tag ids error, because %s", err.Error()) + log.Warnf(c, "[data_managements.getExportedFileContent] parse transaction tag filters error, because %s", err.Error()) return nil, "", errs.Or(err, errs.ErrOperationFailed) } } @@ -395,7 +395,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(exportTransactionDataReq.MinTime) } - allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, exportTransactionDataReq.TagFilterType, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true) + allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true) if err != nil { log.Errorf(c, "[data_managements.getExportedFileContent] failed to all transactions user \"uid:%d\", because %s", uid, err.Error()) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index aa74e87c..e9477780 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -83,19 +83,19 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.WebContext) (any, *err return nil, errs.Or(err, errs.ErrOperationFailed) } - var allTagIds []int64 - noTags := transactionCountReq.TagIds == "none" + noTags := transactionCountReq.TagFilter == models.TransactionNoTagFilterValue + var tagFilters []*models.TransactionTagFilter if !noTags { - allTagIds, err = a.transactionTags.GetTagIds(transactionCountReq.TagIds) + tagFilters, err = models.ParseTransactionTagFilter(transactionCountReq.TagFilter) if err != nil { - log.Warnf(c, "[transactions.TransactionCountHandler] get transaction tag ids error, because %s", err.Error()) + log.Warnf(c, "[transactions.TransactionCountHandler] parse transaction filters error, because %s", err.Error()) return nil, errs.Or(err, errs.ErrOperationFailed) } } - totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionCountReq.TagFilterType, transactionCountReq.AmountFilter, transactionCountReq.Keyword) + totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, 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()) @@ -151,14 +151,14 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs return nil, errs.Or(err, errs.ErrOperationFailed) } - var allTagIds []int64 - noTags := transactionListReq.TagIds == "none" + noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue + var tagFilters []*models.TransactionTagFilter if !noTags { - allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds) + tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter) if err != nil { - log.Warnf(c, "[transactions.TransactionListHandler] get transaction tag ids error, because %s", err.Error()) + log.Warnf(c, "[transactions.TransactionListHandler] parse transaction tag filters error, because %s", err.Error()) return nil, errs.Or(err, errs.ErrOperationFailed) } } @@ -166,7 +166,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.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword) + totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, 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()) @@ -174,7 +174,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.TagFilterType, 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, tagFilters, noTags, 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()) @@ -254,19 +254,19 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any, return nil, errs.Or(err, errs.ErrOperationFailed) } - var allTagIds []int64 - noTags := transactionListReq.TagIds == "none" + noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue + var tagFilters []*models.TransactionTagFilter if !noTags { - allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds) + tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter) if err != nil { - log.Warnf(c, "[transactions.TransactionMonthListHandler] get transaction tag ids error, because %s", err.Error()) + log.Warnf(c, "[transactions.TransactionMonthListHandler] parse transaction tag filters error, because %s", err.Error()) return nil, errs.Or(err, errs.ErrOperationFailed) } } - transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword) + transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, 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()) @@ -413,20 +413,20 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, return nil, errs.ErrClientTimezoneOffsetInvalid } - var allTagIds []int64 - noTags := statisticReq.TagIds == "none" + noTags := statisticReq.TagFilter == models.TransactionNoTagFilterValue + var tagFilters []*models.TransactionTagFilter if !noTags { - allTagIds, err = a.transactionTags.GetTagIds(statisticReq.TagIds) + tagFilters, err = models.ParseTransactionTagFilter(statisticReq.TagFilter) if err != nil { - log.Warnf(c, "[transactions.TransactionStatisticsHandler] get transaction tag ids error, because %s", err.Error()) + log.Warnf(c, "[transactions.TransactionStatisticsHandler] parse transaction tag filters error, because %s", err.Error()) return nil, errs.Or(err, errs.ErrOperationFailed) } } uid := c.GetCurrentUid() - totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone) + totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, tagFilters, noTags, statisticReq.Keyword, 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()) @@ -481,20 +481,20 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext) return nil, errs.Or(err, errs.ErrOperationFailed) } - var allTagIds []int64 - noTags := statisticTrendsReq.TagIds == "none" + noTags := statisticTrendsReq.TagFilter == models.TransactionNoTagFilterValue + var tagFilters []*models.TransactionTagFilter if !noTags { - allTagIds, err = a.transactionTags.GetTagIds(statisticTrendsReq.TagIds) + tagFilters, err = models.ParseTransactionTagFilter(statisticTrendsReq.TagFilter) if err != nil { - log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] get transaction tag ids error, because %s", err.Error()) + log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] parse transaction tag filters error, because %s", err.Error()) return nil, errs.Or(err, errs.ErrOperationFailed) } } uid := c.GetCurrentUid() - allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone) + allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, tagFilters, noTags, statisticTrendsReq.Keyword, 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/errs/global.go b/pkg/errs/global.go index 9d2690a4..6df2539d 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -94,3 +94,8 @@ func GetParameterInvalidHexRGBColorMessage(field string) string { func GetParameterInvalidAmountFilterMessage(field string) string { return fmt.Sprintf("parameter \"%s\" is invalid amount filter", field) } + +// GetParameterInvalidTagFilterMessage returns specific error message for invalid tag filter parameter error +func GetParameterInvalidTagFilterMessage(field string) string { + return fmt.Sprintf("parameter \"%s\" is invalid tag filter", field) +} diff --git a/pkg/mcp/query_transactions_tool_handler.go b/pkg/mcp/query_transactions_tool_handler.go index 41793652..acd6c1ca 100644 --- a/pkg/mcp/query_transactions_tool_handler.go +++ b/pkg/mcp/query_transactions_tool_handler.go @@ -153,14 +153,14 @@ func (h *mcpQueryTransactionsToolHandler) Handle(c *core.WebContext, callToolReq } } - totalCount, err := services.GetTransactionService().GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", queryTransactionsRequest.Keyword) + totalCount, err := services.GetTransactionService().GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword) if err != nil { log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) return nil, nil, err } - transactions, err := services.GetTransactionService().GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", queryTransactionsRequest.Keyword, queryTransactionsRequest.Page, queryTransactionsRequest.Count, false, true) + transactions, err := services.GetTransactionService().GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword, queryTransactionsRequest.Page, queryTransactionsRequest.Count, false, true) structuredResponse, response, err := h.createNewMCPQueryTransactionsResponse(c, &queryTransactionsRequest, transactions, totalCount, services.GetAccountService().GetAccountMapByList(allAccounts), services.GetTransactionCategoryService().GetCategoryMapByList(allCategories)) if err != nil { diff --git a/pkg/models/data_management.go b/pkg/models/data_management.go index f55ddb5f..02dc643d 100644 --- a/pkg/models/data_management.go +++ b/pkg/models/data_management.go @@ -24,13 +24,12 @@ type DataStatisticsResponse struct { // ExportTransactionDataRequest represents export transaction request type ExportTransactionDataRequest struct { - Type TransactionType `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"` // Unix timestamp in seconds - MinTime int64 `form:"min_time" binding:"min=0"` // Unix timestamp in seconds + Type TransactionType `form:"type" binding:"min=0,max=4"` + CategoryIds string `form:"category_ids"` + AccountIds string `form:"account_ids"` + TagFilter string `form:"tag_filter" binding:"validTagFilter"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` + Keyword string `form:"keyword"` + MaxTime int64 `form:"max_time" binding:"min=0"` // Unix timestamp in seconds + MinTime int64 `form:"min_time" binding:"min=0"` // Unix timestamp in seconds } diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 9a47e672..42e983bd 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -104,6 +104,9 @@ func (t TransactionDbType) ToTransactionRelatedAccountType() (TransactionRelated } } +// TransactionTagFilterValue represents transaction tag filter value for no tag +const TransactionNoTagFilterValue = "none" + // TransactionTagFilterType represents transaction tag filter type type TransactionTagFilterType byte @@ -199,54 +202,56 @@ type TransactionImportProcessRequest struct { ClientSessionId string `form:"client_session_id"` } +type TransactionTagFilter struct { + TagIds []int64 + Type TransactionTagFilterType +} + // TransactionCountRequest represents transaction count request type TransactionCountRequest struct { - Type TransactionType `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"` // Transaction time sequence id - MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id + Type TransactionType `form:"type" binding:"min=0,max=4"` + CategoryIds string `form:"category_ids"` + AccountIds string `form:"account_ids"` + TagFilter string `form:"tag_filter" binding:"validTagFilter"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` + Keyword string `form:"keyword"` + MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id + MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id } // TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request type TransactionListByMaxTimeRequest struct { - Type TransactionType `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"` // Transaction time sequence id - MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id - 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 TransactionType `form:"type" binding:"min=0,max=4"` + CategoryIds string `form:"category_ids"` + AccountIds string `form:"account_ids"` + TagFilter string `form:"tag_filter" binding:"validTagFilter"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` + Keyword string `form:"keyword"` + MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id + MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id + 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 TransactionType `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"` + Year int32 `form:"year" binding:"required,min=1"` + Month int32 `form:"month" binding:"required,min=1"` + Type TransactionType `form:"type" binding:"min=0,max=4"` + CategoryIds string `form:"category_ids"` + AccountIds string `form:"account_ids"` + TagFilter string `form:"tag_filter" binding:"validTagFilter"` + 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"` } // TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request @@ -258,21 +263,19 @@ type TransactionReconciliationStatementRequest 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"` - TagIds string `form:"tag_ids"` - TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"` - Keyword string `form:"keyword"` - UseTransactionTimezone bool `form:"use_transaction_timezone"` + StartTime int64 `form:"start_time" binding:"min=0"` + EndTime int64 `form:"end_time" binding:"min=0"` + TagFilter string `form:"tag_filter" binding:"validTagFilter"` + Keyword string `form:"keyword"` + UseTransactionTimezone bool `form:"use_transaction_timezone"` } // TransactionStatisticTrendsRequest represents all parameters of transaction statistic trends request type TransactionStatisticTrendsRequest struct { YearMonthRangeRequest - TagIds string `form:"tag_ids"` - TagFilterType TransactionTagFilterType `form:"tag_filter_type" binding:"min=0,max=3"` - Keyword string `form:"keyword"` - UseTransactionTimezone bool `form:"use_transaction_timezone"` + TagFilter string `form:"tag_filter" binding:"validTagFilter"` + Keyword string `form:"keyword"` + UseTransactionTimezone bool `form:"use_transaction_timezone"` } // TransactionStatisticAssetTrendsRequest represents all parameters of transaction statistic asset trends request @@ -445,6 +448,52 @@ type TransactionAmountsResponseItemAmountInfo struct { ExpenseAmount int64 `json:"expenseAmount"` } +// ParseTransactionTagFilter parses transaction tag filter from string +func ParseTransactionTagFilter(tagFilterStr string) ([]*TransactionTagFilter, error) { + if tagFilterStr == "" || tagFilterStr == TransactionNoTagFilterValue { + return []*TransactionTagFilter{}, nil + } + + filters := strings.Split(tagFilterStr, ";") + transactionTagFilters := make([]*TransactionTagFilter, 0, len(filters)) + + for _, filter := range filters { + tagFilterItem := strings.Split(filter, ":") + + if len(tagFilterItem) != 2 { + return nil, errs.ErrFormatInvalid + } + + tagFilterType, err := utils.StringToInt(tagFilterItem[0]) + + if err != nil || (tagFilterType < int(TRANSACTION_TAG_FILTER_HAS_ANY) || tagFilterType > int(TRANSACTION_TAG_FILTER_NOT_HAS_ALL)) { + return nil, errs.ErrFormatInvalid + } + + textualTagIds := strings.Split(tagFilterItem[1], ",") + tagIds := make([]int64, 0, len(textualTagIds)) + + for _, tagIdStr := range textualTagIds { + tagId, err := utils.StringToInt64(tagIdStr) + + if err != nil { + return nil, errs.ErrTransactionTagIdInvalid + } + + tagIds = append(tagIds, tagId) + } + + transactionTagFilter := &TransactionTagFilter{ + TagIds: tagIds, + Type: TransactionTagFilterType(tagFilterType), + } + + transactionTagFilters = append(transactionTagFilters, transactionTagFilter) + } + + return transactionTagFilters, nil +} + // IsEditable returns whether this transaction can be edited func (t *Transaction) IsEditable(currentUser *User, utcOffset int16, account *Account, relatedAccount *Account) bool { if currentUser == nil || !currentUser.CanEditTransactionByTransactionTime(t.TransactionTime, utcOffset) { diff --git a/pkg/models/transaction_test.go b/pkg/models/transaction_test.go index 078908d7..325f977f 100644 --- a/pkg/models/transaction_test.go +++ b/pkg/models/transaction_test.go @@ -9,6 +9,101 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/errs" ) +func TestParseTransactionTagFilter_EmptyTagFilter(t *testing.T) { + actualValue, err := ParseTransactionTagFilter("") + assert.Nil(t, err) + assert.Equal(t, 0, len(actualValue)) +} + +func TestParseTransactionTagFilter_NoTag(t *testing.T) { + actualValue, err := ParseTransactionTagFilter("none") + assert.Nil(t, err) + assert.Equal(t, 0, len(actualValue)) +} + +func TestParseTransactionTagFilter_NoValidFilter(t *testing.T) { + _, err := ParseTransactionTagFilter(";") + assert.EqualError(t, err, errs.ErrFormatInvalid.Message) + + _, err = ParseTransactionTagFilter(";;") + assert.EqualError(t, err, errs.ErrFormatInvalid.Message) +} + +func TestParseTransactionTagFilter_ValidOneFilterInTagFilters(t *testing.T) { + actualValue, err := ParseTransactionTagFilter("0:1") + assert.Nil(t, err) + assert.Equal(t, 1, len(actualValue)) + assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ANY, actualValue[0].Type) + assert.Equal(t, 1, len(actualValue[0].TagIds)) + assert.Equal(t, []int64{1}, actualValue[0].TagIds) + + actualValue, err = ParseTransactionTagFilter("0:1,2,3") + assert.Nil(t, err) + assert.Equal(t, 1, len(actualValue)) + assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ANY, actualValue[0].Type) + assert.Equal(t, 3, len(actualValue[0].TagIds)) + assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds) + + actualValue, err = ParseTransactionTagFilter("1:1,2,3") + assert.Nil(t, err) + assert.Equal(t, 1, len(actualValue)) + assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ALL, actualValue[0].Type) + assert.Equal(t, 3, len(actualValue[0].TagIds)) + assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds) + + actualValue, err = ParseTransactionTagFilter("2:1,2,3") + assert.Nil(t, err) + assert.Equal(t, 1, len(actualValue)) + assert.Equal(t, TRANSACTION_TAG_FILTER_NOT_HAS_ANY, actualValue[0].Type) + assert.Equal(t, 3, len(actualValue[0].TagIds)) + assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds) + + actualValue, err = ParseTransactionTagFilter("3:1,2,3") + assert.Nil(t, err) + assert.Equal(t, 1, len(actualValue)) + assert.Equal(t, TRANSACTION_TAG_FILTER_NOT_HAS_ALL, actualValue[0].Type) + assert.Equal(t, 3, len(actualValue[0].TagIds)) + assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds) +} + +func TestParseTransactionTagFilter_InvalidTagFilterType(t *testing.T) { + _, err := ParseTransactionTagFilter("a:1,2,3") + assert.EqualError(t, err, errs.ErrFormatInvalid.Message) + + _, err = ParseTransactionTagFilter("-1:1,2,3") + assert.EqualError(t, err, errs.ErrFormatInvalid.Message) + + _, err = ParseTransactionTagFilter("4:1,2,3") + assert.EqualError(t, err, errs.ErrFormatInvalid.Message) +} + +func TestParseTransactionTagFilter_NoTagIdsInFilter(t *testing.T) { + _, err := ParseTransactionTagFilter("0") + assert.EqualError(t, err, errs.ErrFormatInvalid.Message) + + _, err = ParseTransactionTagFilter("0:") + assert.EqualError(t, err, errs.ErrTransactionTagIdInvalid.Message) +} + +func TestParseTransactionTagFilter_InvalidTagIdsInFilter(t *testing.T) { + _, err := ParseTransactionTagFilter("0:abc") + assert.EqualError(t, err, errs.ErrTransactionTagIdInvalid.Message) +} + +func TestParseTransactionTagFilter_ValidTwoFilterInTagFilters(t *testing.T) { + actualValue, err := ParseTransactionTagFilter("0:1,2,3;2:4,5,6") + assert.Nil(t, err) + assert.Equal(t, 2, len(actualValue)) + + assert.Equal(t, TRANSACTION_TAG_FILTER_HAS_ANY, actualValue[0].Type) + assert.Equal(t, 3, len(actualValue[0].TagIds)) + assert.Equal(t, []int64{1, 2, 3}, actualValue[0].TagIds) + + assert.Equal(t, TRANSACTION_TAG_FILTER_NOT_HAS_ANY, actualValue[1].Type) + assert.Equal(t, 3, len(actualValue[1].TagIds)) + assert.Equal(t, []int64{4, 5, 6}, actualValue[1].TagIds) +} + func TestTransactionAmountsRequestGetTransactionAmountsRequestItems(t *testing.T) { transactionAmountsRequest := &TransactionAmountsRequest{ Query: "name1_1234567890_1234567891|name2_1234567900_1234567901", diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index fb8438e4..68d373c6 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -76,11 +76,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, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, count, false, noDuplicated) + return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, count, false, noDuplicated) } // GetAllSpecifiedTransactions returns all transactions that match given conditions -func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, pageCount int32, noDuplicated bool) ([]*models.Transaction, error) { +func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, pageCount int32, noDuplicated bool) ([]*models.Transaction, error) { if maxTransactionTime <= 0 { maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix()) } @@ -88,7 +88,7 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int var allTransactions []*models.Transaction for maxTransactionTime > 0 { - transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, noTags, tagFilterType, amountFilter, keyword, 1, pageCount, false, noDuplicated) + transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagFilters, noTags, amountFilter, keyword, 1, pageCount, false, noDuplicated) if err != nil { return nil, err @@ -116,7 +116,7 @@ func (s *TransactionService) GetAllTransactionsInOneAccountWithAccountBalanceByM var allTransactions []*models.Transaction for maxTransactionTime > 0 { - transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCount, false, true) + transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, "", "", 1, pageCount, false, true) if err != nil { return nil, 0, 0, 0, 0, err @@ -207,7 +207,7 @@ func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core. var allTransactions []*models.Transaction for maxTransactionTime > 0 { - transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCountForLoadTransactionAmounts, false, false) + transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, pageCountForLoadTransactionAmounts, false, false) if err != nil { return nil, err @@ -324,7 +324,7 @@ func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core. } // GetTransactionsByMaxTime returns transactions before given time -func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, 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) { +func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -358,9 +358,9 @@ func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, actualCount++ } - condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagIds, amountFilter, keyword, noDuplicated) + condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, noDuplicated) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) - sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) err = sess.Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions) @@ -368,7 +368,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.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string) ([]*models.Transaction, error) { +func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid int64, year int32, month int32, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -392,9 +392,9 @@ func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid in var transactions []*models.Transaction - condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagIds, amountFilter, keyword, true) + condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) - sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) err = sess.OrderBy("transaction_time desc").Find(&transactions) @@ -437,11 +437,11 @@ 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, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "") + return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "") } // GetTransactionCount returns count of transactions -func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string) (int64, error) { +func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string) (int64, error) { if uid <= 0 { return 0, errs.ErrUserIdInvalid } @@ -457,9 +457,9 @@ func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxT } } - condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagIds, amountFilter, keyword, true) + condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true) sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...) - sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) return sess.Count(&models.Transaction{}) } @@ -1730,7 +1730,7 @@ func (s *TransactionService) DeleteAllTransactionsOfAccount(c core.Context, uid return errs.ErrAccountIdInvalid } - transactions, err := s.GetAllSpecifiedTransactions(c, uid, 0, 0, 0, nil, []int64{accountId}, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", pageCount, true) + transactions, err := s.GetAllSpecifiedTransactions(c, uid, 0, 0, 0, nil, []int64{accountId}, nil, false, "", "", pageCount, true) if err != nil { return err @@ -1923,7 +1923,7 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c core.Context, ui } // GetAccountsAndCategoriesTotalInflowAndOutflow returns the every accounts and categories total inflows and outflows amount by specific date range -func (s *TransactionService) GetAccountsAndCategoriesTotalInflowAndOutflow(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) { +func (s *TransactionService) GetAccountsAndCategoriesTotalInflowAndOutflow(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, tagFilters []*models.TransactionTagFilter, noTags bool, keyword string, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -1979,7 +1979,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalInflowAndOutflow(c cor } sess := s.UserDataDB(uid).NewSession(c).Select("type, category_id, account_id, related_account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) - sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) @@ -2046,7 +2046,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalInflowAndOutflow(c cor } // GetAccountsAndCategoriesMonthlyInflowAndOutflow returns the every accounts monthly inflows and outflows amount by specific date range -func (s *TransactionService) GetAccountsAndCategoriesMonthlyInflowAndOutflow(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) { +func (s *TransactionService) GetAccountsAndCategoriesMonthlyInflowAndOutflow(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, tagFilters []*models.TransactionTagFilter, noTags bool, keyword string, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -2107,7 +2107,7 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyInflowAndOutflow(c c } sess := s.UserDataDB(uid).NewSession(c).Select("type, category_id, account_id, related_account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) - sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) + sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags) err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) @@ -2460,7 +2460,7 @@ func (s *TransactionService) doCreateTransaction(c core.Context, database *datas return err } -func (s *TransactionService) buildTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionDbType 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, transactionDbType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, amountFilter string, keyword string, noDuplicated bool) (string, []any) { condition := "uid=? AND deleted=?" conditionParams := make([]any, 0, 16) conditionParams = append(conditionParams, uid) @@ -2616,38 +2616,51 @@ func (s *TransactionService) buildTransactionQueryCondition(uid int64, maxTransa return condition, conditionParams } -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 { - subQueryCondition = subQueryCondition.And(builder.Lte{"transaction_time": maxTransactionTime}) - } - - if minTransactionTime > 0 { - subQueryCondition = subQueryCondition.And(builder.Gte{"transaction_time": minTransactionTime}) - } - +func (s *TransactionService) appendFilterTagIdsConditionToQuery(sess *xorm.Session, uid int64, maxTransactionTime int64, minTransactionTime int64, tagFilters []*models.TransactionTagFilter, noTags bool) *xorm.Session { if noTags { + subQueryCondition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false}) + + if maxTransactionTime > 0 { + subQueryCondition = subQueryCondition.And(builder.Lte{"transaction_time": maxTransactionTime}) + } + + if minTransactionTime > 0 { + subQueryCondition = subQueryCondition.And(builder.Gte{"transaction_time": minTransactionTime}) + } + subQuery := builder.Select("transaction_id").From("transaction_tag_index").Where(subQueryCondition) sess.NotIn("transaction_id", subQuery).NotIn("related_id", subQuery) return sess } - if len(tagIds) < 1 { + if len(tagFilters) < 1 { return sess } - subQueryCondition = subQueryCondition.And(builder.In("tag_id", tagIds)) - subQuery := builder.Select("transaction_id").From("transaction_tag_index").Where(subQueryCondition) + for i := 0; i < len(tagFilters); i++ { + tagFilter := tagFilters[i] + subQueryCondition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false}) - 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 maxTransactionTime > 0 { + subQueryCondition = subQueryCondition.And(builder.Lte{"transaction_time": maxTransactionTime}) + } - if tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ALL { - sess.And(builder.Or(builder.In("transaction_id", subQuery), builder.In("related_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).NotIn("related_id", subQuery) + if minTransactionTime > 0 { + subQueryCondition = subQueryCondition.And(builder.Gte{"transaction_time": minTransactionTime}) + } + + subQueryCondition = subQueryCondition.And(builder.In("tag_id", tagFilter.TagIds)) + subQuery := builder.Select("transaction_id").From("transaction_tag_index").Where(subQueryCondition) + + if tagFilter.Type == models.TRANSACTION_TAG_FILTER_HAS_ALL || tagFilter.Type == models.TRANSACTION_TAG_FILTER_NOT_HAS_ALL { + subQuery = subQuery.GroupBy("transaction_id").Having(fmt.Sprintf("COUNT(DISTINCT tag_id) >= %d", len(tagFilter.TagIds))) + } + + if tagFilter.Type == models.TRANSACTION_TAG_FILTER_HAS_ANY || tagFilter.Type == models.TRANSACTION_TAG_FILTER_HAS_ALL { + sess.And(builder.Or(builder.In("transaction_id", subQuery), builder.In("related_id", subQuery))) + } else if tagFilter.Type == models.TRANSACTION_TAG_FILTER_NOT_HAS_ANY || tagFilter.Type == models.TRANSACTION_TAG_FILTER_NOT_HAS_ALL { + sess.NotIn("transaction_id", subQuery).NotIn("related_id", subQuery) + } } return sess diff --git a/pkg/utils/api.go b/pkg/utils/api.go index a7ed66d4..19ac5ecd 100644 --- a/pkg/utils/api.go +++ b/pkg/utils/api.go @@ -186,6 +186,8 @@ func getValidationErrorText(err validator.FieldError) string { return errs.GetParameterInvalidHexRGBColorMessage(fieldName) case "validAmountFilter": return errs.GetParameterInvalidAmountFilterMessage(fieldName) + case "validTagFilter": + return errs.GetParameterInvalidTagFilterMessage(fieldName) } return errs.GetParameterInvalidMessage(fieldName) diff --git a/pkg/validators/tag_filter.go b/pkg/validators/tag_filter.go new file mode 100644 index 00000000..741e391e --- /dev/null +++ b/pkg/validators/tag_filter.go @@ -0,0 +1,26 @@ +package validators + +import ( + "github.com/go-playground/validator/v10" + + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +// ValidTagFilter returns whether the given tag filter is valid +func ValidTagFilter(fl validator.FieldLevel) bool { + if value, ok := fl.Field().Interface().(string); ok { + if value == "" { + return true + } + + if value == models.TransactionNoTagFilterValue { + return true + } + + _, err := models.ParseTransactionTagFilter(value) + + return err == nil + } + + return false +} diff --git a/pkg/validators/tag_filter_test.go b/pkg/validators/tag_filter_test.go new file mode 100644 index 00000000..efbe693b --- /dev/null +++ b/pkg/validators/tag_filter_test.go @@ -0,0 +1,104 @@ +package validators + +import ( + "testing" + + "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/assert" +) + +func TestEmptyTagFilter(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var("", "validTagFilter") + assert.Nil(t, err) +} + +func TestNoTag(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var("none", "validTagFilter") + assert.Nil(t, err) +} + +func TestNoValidFilter(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var(";", "validTagFilter") + assert.NotNil(t, err) + + err = validate.Var(";;", "validTagFilter") + assert.NotNil(t, err) +} + +func TestValidOneFilterInTagFilters(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var("0:1", "validTagFilter") + assert.Nil(t, err) + + err = validate.Var("0:1,2,3", "validTagFilter") + assert.Nil(t, err) + + err = validate.Var("1:1,2,3", "validTagFilter") + assert.Nil(t, err) + + err = validate.Var("2:1,2,3", "validTagFilter") + assert.Nil(t, err) + + err = validate.Var("3:1,2,3", "validTagFilter") + assert.Nil(t, err) +} + +func TestInvalidTagFilterType(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var("a:1,2,3", "validTagFilter") + assert.NotNil(t, err) + + err = validate.Var("-1:1,2,3", "validTagFilter") + assert.NotNil(t, err) + + err = validate.Var("4:1,2,3", "validTagFilter") + assert.NotNil(t, err) +} + +func TestNoTagIdsInFilter(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var("0", "validTagFilter") + assert.NotNil(t, err) + + err = validate.Var("0:", "validTagFilter") + assert.NotNil(t, err) +} + +func TestInvalidTagIdsInFilter(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var("0:abc", "validTagFilter") + assert.NotNil(t, err) +} + +func TestValidTwoFilterInTagFilters(t *testing.T) { + validate := validator.New() + err := validate.RegisterValidation("validTagFilter", ValidTagFilter) + assert.Nil(t, err) + + err = validate.Var("0:1,2,3;2:4,5,6", "validTagFilter") + assert.Nil(t, err) +} diff --git a/src/consts/api.ts b/src/consts/api.ts index 115de41d..03259d34 100644 --- a/src/consts/api.ts +++ b/src/consts/api.ts @@ -162,5 +162,13 @@ export const PARAMETERIZED_ERRORS: ParameterizedError[] = [ field: 'parameter', localized: true }] + }, + { + localeKey: 'parameter invalid tag filter', + regex: /^parameter "(\w+)" is invalid tag filter$/, + parameters: [{ + field: 'parameter', + localized: true + }] } ]; diff --git a/src/core/transaction.ts b/src/core/transaction.ts index 665b459e..1f9700e9 100644 --- a/src/core/transaction.ts +++ b/src/core/transaction.ts @@ -40,13 +40,12 @@ export class TransactionEditScopeType implements TypeAndName { export class TransactionTagFilterType implements TypeAndName { private static readonly allInstances: TransactionTagFilterType[] = []; + private static readonly allInstancesByType: Record = {}; - public static readonly HasAny = new TransactionTagFilterType(0, 'With Any Selected Tags'); - public static readonly HasAll = new TransactionTagFilterType(1, 'With All Selected Tags'); - public static readonly NotHasAny = new TransactionTagFilterType(2, 'Without Any Selected Tags'); - public static readonly NotHasAll = new TransactionTagFilterType(3, 'Without All Selected Tags'); - - public static readonly Default = TransactionTagFilterType.HasAny; + public static readonly HasAny = new TransactionTagFilterType(0, 'Include Any Selected Tags'); + public static readonly HasAll = new TransactionTagFilterType(1, 'Include All Selected Tags'); + public static readonly NotHasAny = new TransactionTagFilterType(2, 'Exclude Any Selected Tags'); + public static readonly NotHasAll = new TransactionTagFilterType(3, 'Exclude All Selected Tags'); public readonly type: number; public readonly name: string; @@ -56,9 +55,14 @@ export class TransactionTagFilterType implements TypeAndName { this.name = name; TransactionTagFilterType.allInstances.push(this); + TransactionTagFilterType.allInstancesByType[type] = this; } public static values(): TransactionTagFilterType[] { return TransactionTagFilterType.allInstances; } + + public static parse(type: number): TransactionTagFilterType | undefined { + return TransactionTagFilterType.allInstancesByType[type]; + } } diff --git a/src/lib/services.ts b/src/lib/services.ts index a07622f8..d377e690 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -417,11 +417,12 @@ export default { let params = ''; if (req) { + const tagFilter = encodeURIComponent(req.tagFilter); const amountFilter = encodeURIComponent(req.amountFilter); const keyword = encodeURIComponent(req.keyword); - params = `max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_ids=${req.tagIds}&tag_filter_type=${req.tagFilterType}&amount_filter=${amountFilter}&keyword=${keyword}`; + params = `max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}`; } else { - params = 'max_time=0&min_time=0&type=0&category_ids=&account_ids=&tag_ids=&tag_filter_type=0&amount_filter=&keyword='; + params = 'max_time=0&min_time=0&type=0&category_ids=&account_ids=&tag_filter=&amount_filter=&keyword='; } if (fileType === 'csv') { @@ -476,14 +477,16 @@ export default { return axios.post>('v1/accounts/sub_account/delete.json', req); }, getTransactions: (req: TransactionListByMaxTimeRequest): ApiResponsePromise => { + const tagFilter = encodeURIComponent(req.tagFilter); const amountFilter = encodeURIComponent(req.amountFilter); const keyword = encodeURIComponent(req.keyword); - return axios.get>(`v1/transactions/list.json?max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_ids=${req.tagIds}&tag_filter_type=${req.tagFilterType}&amount_filter=${amountFilter}&keyword=${keyword}&count=${req.count}&page=${req.page}&with_count=${req.withCount}&trim_account=true&trim_category=true&trim_tag=true`); + return axios.get>(`v1/transactions/list.json?max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&count=${req.count}&page=${req.page}&with_count=${req.withCount}&trim_account=true&trim_category=true&trim_tag=true`); }, getAllTransactionsByMonth: (req: TransactionListInMonthByPageRequest): ApiResponsePromise => { + const tagFilter = encodeURIComponent(req.tagFilter); const amountFilter = encodeURIComponent(req.amountFilter); const keyword = encodeURIComponent(req.keyword); - return axios.get>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_ids=${req.tagIds}&tag_filter_type=${req.tagFilterType}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); + return axios.get>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); }, getReconciliationStatements: (req: TransactionReconciliationStatementRequest): ApiResponsePromise => { return axios.get>(`v1/transactions/reconciliation_statements.json?account_id=${req.accountId}&start_time=${req.startTime}&end_time=${req.endTime}`); @@ -499,12 +502,8 @@ export default { queryParams.push(`end_time=${req.endTime}`); } - if (req.tagIds) { - queryParams.push(`tag_ids=${req.tagIds}`); - } - - if (req.tagFilterType) { - queryParams.push(`tag_filter_type=${req.tagFilterType}`); + if (req.tagFilter) { + queryParams.push(`tag_filter=${encodeURIComponent(req.tagFilter)}`); } if (req.keyword) { @@ -524,12 +523,8 @@ export default { queryParams.push(`end_year_month=${req.endYearMonth}`); } - if (req.tagIds) { - queryParams.push(`tag_ids=${req.tagIds}`); - } - - if (req.tagFilterType) { - queryParams.push(`tag_filter_type=${req.tagFilterType}`); + if (req.tagFilter) { + queryParams.push(`tag_filter=${encodeURIComponent(req.tagFilter)}`); } if (req.keyword) { diff --git a/src/locales/de.json b/src/locales/de.json index db63a6a2..07e56c4d 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1300,7 +1300,7 @@ "balanceTime": "Saldozeit", "startTime": "Startzeit", "endTime": "Endzeit", - "tagFilterType": "Tag-Filtertyp", + "tagFilter": "Tag Filter", "amountFilter": "Betragsfilter", "sourceAccountId": "Quellkonto-ID", "destinationAccountId": "Zielkonto-ID", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} hat ein ungültiges Format", "parameter invalid currency": "{parameter} hat ein ungültiges Format", "parameter invalid color": "{parameter} hat ein ungültiges Format", - "parameter invalid amount filter": "{parameter} hat ein ungültiges Format" + "parameter invalid amount filter": "{parameter} hat ein ungültiges Format", + "parameter invalid tag filter": "{parameter} hat ein ungültiges Format" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Auto detect", "Miscellaneous": "Verschiedenes", "Default": "Standard", + "Included": "Included", + "Excluded": "Excluded", "Done": "Fertig", "Continue": "Weiter", "Previous": "Zurück", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Auswahl auf dieser Seite umkehren", "Select All Valid Items": "Alle gültigen Elemente auswählen", "Select All Invalid Items": "Alle ungültigen Elemente auswählen", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Zurück", "Load More": "Mehr laden", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "Zielkonto", "Transaction Tag": "Transaction Tag", "Without Tags": "Ohne Tags", - "With Any Selected Tags": "Mit ausgewählten Tags", - "With All Selected Tags": "Mit allen ausgewählten Tags", - "Without Any Selected Tags": "Ohne ausgewählte Tags", - "Without All Selected Tags": "Ohne alle ausgewählten Tags", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Mehrere Tags", "Transaction Time": "Transaktionszeit", "Scheduled Transaction Frequency": "Häufigkeit der geplanten Transaktion", diff --git a/src/locales/en.json b/src/locales/en.json index 1a0e09b8..ff98f4be 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1300,7 +1300,7 @@ "balanceTime": "Balance Time", "startTime": "Start Time", "endTime": "End Time", - "tagFilterType": "Tag Filter Type", + "tagFilter": "Tag Filter", "amountFilter": "Amount Filter", "sourceAccountId": "Source Account ID", "destinationAccountId": "Destination Account ID", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} is invalid format", "parameter invalid currency": "{parameter} is invalid format", "parameter invalid color": "{parameter} is invalid format", - "parameter invalid amount filter": "{parameter} is invalid format" + "parameter invalid amount filter": "{parameter} is invalid format", + "parameter invalid tag filter": "{parameter} is invalid format" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Auto detect", "Miscellaneous": "Miscellaneous", "Default": "Default", + "Included": "Included", + "Excluded": "Excluded", "Done": "Done", "Continue": "Continue", "Previous": "Previous", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Invert Selection in This Page", "Select All Valid Items": "Select All Valid Items", "Select All Invalid Items": "Select All Invalid Items", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Back", "Load More": "Load More", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "Destination Account", "Transaction Tag": "Transaction Tag", "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", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Multiple Tags", "Transaction Time": "Transaction Time", "Scheduled Transaction Frequency": "Scheduled Transaction Frequency", diff --git a/src/locales/es.json b/src/locales/es.json index fed84993..cc4067a8 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1300,7 +1300,7 @@ "balanceTime": "Tiempo de equilibrio", "startTime": "Hora de inicio", "endTime": "Hora de finalización", - "tagFilterType": "Tipo de filtro de etiquetas", + "tagFilter": "Tag Filter", "amountFilter": "Filtro de cantidad", "sourceAccountId": "ID de cuenta de origen", "destinationAccountId": "ID de cuenta de destino", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} es un formato no válido", "parameter invalid currency": "{parameter} es un formato no válido", "parameter invalid color": "{parameter} es un formato no válido", - "parameter invalid amount filter": "{parameter} es un formato no válido" + "parameter invalid amount filter": "{parameter} es un formato no válido", + "parameter invalid tag filter": "{parameter} es un formato no válido" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Auto detect", "Miscellaneous": "Misceláneas", "Default": "Por defecto", + "Included": "Included", + "Excluded": "Excluded", "Done": "Hecho", "Continue": "Continuar", "Previous": "Anterior", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Invertir selección en esta página", "Select All Valid Items": "Seleccionar todos los artículos válidos", "Select All Invalid Items": "Seleccionar todos los artículos no válidos", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Atrás", "Load More": "Cargar más", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "Cuenta de destino", "Transaction Tag": "Transaction Tag", "Without Tags": "Sin Etiquetas", - "With Any Selected Tags": "Con alguna de las etiquetas seleccionada", - "With All Selected Tags": "Con todas las etiquetas seleccionadas", - "Without Any Selected Tags": "Sin alguna de las etiquetas seleccionadas", - "Without All Selected Tags": "Sin todas las etiquetas seleccionadas", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Múltiples etiquetas", "Transaction Time": "Tiempo de transacción", "Scheduled Transaction Frequency": "Frecuencia de transacciones programadas", diff --git a/src/locales/fr.json b/src/locales/fr.json index ca29eee7..2b74217a 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1300,7 +1300,7 @@ "balanceTime": "Heure du solde", "startTime": "Heure de début", "endTime": "Heure de fin", - "tagFilterType": "Type de filtre d'étiquette", + "tagFilter": "Tag Filter", "amountFilter": "Filtre de montant", "sourceAccountId": "ID du compte source", "destinationAccountId": "ID du compte de destination", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} a un format invalide", "parameter invalid currency": "{parameter} a un format invalide", "parameter invalid color": "{parameter} a un format invalide", - "parameter invalid amount filter": "{parameter} a un format invalide" + "parameter invalid amount filter": "{parameter} a un format invalide", + "parameter invalid tag filter": "{parameter} a un format invalide" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Détection automatique", "Miscellaneous": "Divers", "Default": "Par défaut", + "Included": "Included", + "Excluded": "Excluded", "Done": "Terminé", "Continue": "Continuer", "Previous": "Précédent", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Inverser la sélection dans cette page", "Select All Valid Items": "Sélectionner tous les éléments valides", "Select All Invalid Items": "Sélectionner tous les éléments invalides", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Retour", "Load More": "Charger plus", "Export Results": "Exporter les résultats", @@ -1802,10 +1811,10 @@ "Destination Account": "Compte de destination", "Transaction Tag": "Étiquette de transaction", "Without Tags": "Sans étiquettes", - "With Any Selected Tags": "Avec n'importe laquelle des étiquettes sélectionnées", - "With All Selected Tags": "Avec toutes les étiquettes sélectionnées", - "Without Any Selected Tags": "Sans aucune des étiquettes sélectionnées", - "Without All Selected Tags": "Sans toutes les étiquettes sélectionnées", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Étiquettes multiples", "Transaction Time": "Heure de transaction", "Scheduled Transaction Frequency": "Fréquence de transaction programmée", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index b2f75048..ec8db763 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -122,8 +122,7 @@ import { } from '@/core/category.ts'; import { - TransactionEditScopeType, - TransactionTagFilterType + TransactionEditScopeType } from '@/core/transaction.ts'; import { @@ -2357,7 +2356,6 @@ export function useI18n() { getAllStatisticsDateAggregationTypes: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, true), getAllStatisticsDateAggregationTypesWithShortName: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, false), getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()), - getAllTransactionTagFilterTypes: () => getLocalizedDisplayNameAndType(TransactionTagFilterType.values()), getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()), getAllImportTransactionColumnTypes: () => getLocalizedDisplayNameAndType(ImportTransactionColumnType.values()), getAllTransactionDefaultCategories, diff --git a/src/locales/it.json b/src/locales/it.json index 4b00b4a3..a0b1822c 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1300,7 +1300,7 @@ "balanceTime": "Ora saldo", "startTime": "Ora di inizio", "endTime": "Ora di fine", - "tagFilterType": "Tipo filtro tag", + "tagFilter": "Tag Filter", "amountFilter": "Filtro importo", "sourceAccountId": "ID conto di origine", "destinationAccountId": "ID conto di destinazione", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} ha un formato non valido", "parameter invalid currency": "{parameter} ha un formato non valido", "parameter invalid color": "{parameter} ha un formato non valido", - "parameter invalid amount filter": "{parameter} ha un formato non valido" + "parameter invalid amount filter": "{parameter} ha un formato non valido", + "parameter invalid tag filter": "{parameter} ha un formato non valido" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Rilevamento automatico", "Miscellaneous": "Varie", "Default": "Predefinito", + "Included": "Included", + "Excluded": "Excluded", "Done": "Fatto", "Continue": "Continua", "Previous": "Precedente", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Inverti selezione in questa pagina", "Select All Valid Items": "Seleziona tutti gli elementi validi", "Select All Invalid Items": "Seleziona tutti gli elementi non validi", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Indietro", "Load More": "Carica altro", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "Conto di destinazione", "Transaction Tag": "Transaction Tag", "Without Tags": "Senza tag", - "With Any Selected Tags": "Con qualsiasi tag selezionato", - "With All Selected Tags": "Con tutti i tag selezionati", - "Without Any Selected Tags": "Senza alcun tag selezionato", - "Without All Selected Tags": "Senza tutti i tag selezionati", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Tag multipli", "Transaction Time": "Ora transazione", "Scheduled Transaction Frequency": "Frequenza transazione pianificata", diff --git a/src/locales/ja.json b/src/locales/ja.json index 900f8165..e63232bd 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1300,7 +1300,7 @@ "balanceTime": "残高時間", "startTime": "開始時間", "endTime": "終了時間", - "tagFilterType": "タグフィルタータイプ", + "tagFilter": "Tag Filter", "amountFilter": "金額フィルター", "sourceAccountId": "元口座ID", "destinationAccountId": "宛先口座ID", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter}は無効な形式です", "parameter invalid currency": "{parameter}は無効な形式です", "parameter invalid color": "{parameter}は無効な形式です", - "parameter invalid amount filter": "{parameter}は無効な形式です" + "parameter invalid amount filter": "{parameter}は無効な形式です", + "parameter invalid tag filter": "{parameter}は無効な形式です" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "自動検出", "Miscellaneous": "その他", "Default": "デフォルト", + "Included": "Included", + "Excluded": "Excluded", "Done": "完了", "Continue": "続ける", "Previous": "前", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "このページの選択を反転", "Select All Valid Items": "すべての有効なアイテムを選択", "Select All Invalid Items": "すべての無効なアイテムを選択します", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "戻る", "Load More": "さらに読み込む", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "宛先口座", "Transaction Tag": "Transaction Tag", "Without Tags": "タグなし", - "With Any Selected Tags": "選択したタグを含む", - "With All Selected Tags": "選択したすべてのタグを含む", - "Without Any Selected Tags": "タグを選択しない", - "Without All Selected Tags": "全てのタグを選択しない", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "複数のタグ", "Transaction Time": "取引時間", "Scheduled Transaction Frequency": "スケジュールされた取引の頻度", diff --git a/src/locales/ko.json b/src/locales/ko.json index 2e6e91f3..6aad73b2 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1300,7 +1300,7 @@ "balanceTime": "잔액 시간", "startTime": "시작 시간", "endTime": "종료 시간", - "tagFilterType": "태그 필터 유형", + "tagFilter": "Tag Filter", "amountFilter": "금액 필터", "sourceAccountId": "출발 계좌 ID", "destinationAccountId": "도착 계좌 ID", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter}는 유효하지 않은 형식입니다", "parameter invalid currency": "{parameter}는 유효하지 않은 형식입니다", "parameter invalid color": "{parameter}는 유효하지 않은 형식입니다", - "parameter invalid amount filter": "{parameter}는 유효하지 않은 형식입니다" + "parameter invalid amount filter": "{parameter}는 유효하지 않은 형식입니다", + "parameter invalid tag filter": "{parameter}는 유효하지 않은 형식입니다" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "자동 감지", "Miscellaneous": "기타", "Default": "기본값", + "Included": "Included", + "Excluded": "Excluded", "Done": "완료", "Continue": "계속", "Previous": "이전", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "현재 페이지 선택 반전", "Select All Valid Items": "유효한 항목 전체 선택", "Select All Invalid Items": "유효하지 않은 항목 전체 선택", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "뒤로", "Load More": "더 불러오기", "Export Results": "결과 내보내기", @@ -1802,10 +1811,10 @@ "Destination Account": "입금 계좌", "Transaction Tag": "거래 태그", "Without Tags": "태그 없음", - "With Any Selected Tags": "선택한 태그 중 하나와 함께", - "With All Selected Tags": "선택한 모든 태그와 함께", - "Without Any Selected Tags": "선택한 태그 없음", - "Without All Selected Tags": "선택한 모든 태그 없음", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "다중 태그", "Transaction Time": "거래 시간", "Scheduled Transaction Frequency": "예약 거래 빈도", diff --git a/src/locales/nl.json b/src/locales/nl.json index c7c73173..864bb9a3 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1300,7 +1300,7 @@ "balanceTime": "Saldo-tijdstip", "startTime": "Starttijd", "endTime": "Eindtijd", - "tagFilterType": "Tag-filtertype", + "tagFilter": "Tag Filter", "amountFilter": "Bedragfilter", "sourceAccountId": "Bronrekening-ID", "destinationAccountId": "Bestemmingsrekening-ID", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} heeft een ongeldig formaat", "parameter invalid currency": "{parameter} heeft een ongeldig formaat", "parameter invalid color": "{parameter} heeft een ongeldig formaat", - "parameter invalid amount filter": "{parameter} heeft een ongeldig formaat" + "parameter invalid amount filter": "{parameter} heeft een ongeldig formaat", + "parameter invalid tag filter": "{parameter} heeft een ongeldig formaat" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Automatisch detecteren", "Miscellaneous": "Diversen", "Default": "Standaard", + "Included": "Included", + "Excluded": "Excluded", "Done": "Klaar", "Continue": "Doorgaan", "Previous": "Vorige", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Selectie op deze pagina omkeren", "Select All Valid Items": "Alle geldige items selecteren", "Select All Invalid Items": "Alle ongeldige items selecteren", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Terug", "Load More": "Meer laden", "Export Results": "Resultaten exporteren", @@ -1802,10 +1811,10 @@ "Destination Account": "Bestemmingsrekening", "Transaction Tag": "Transactietag", "Without Tags": "Zonder tags", - "With Any Selected Tags": "Met willekeurige geselecteerde tags", - "With All Selected Tags": "Met alle geselecteerde tags", - "Without Any Selected Tags": "Zonder geselecteerde tags", - "Without All Selected Tags": "Zonder alle geselecteerde tags", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Meerdere tags", "Transaction Time": "Transactietijd", "Scheduled Transaction Frequency": "Frequentie geplande transactie", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 080a7a09..a0afd707 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1300,7 +1300,7 @@ "balanceTime": "Hora do Saldo", "startTime": "Hora de Início", "endTime": "Hora de Término", - "tagFilterType": "Tipo de Filtro de Tag", + "tagFilter": "Tag Filter", "amountFilter": "Filtro de Quantia", "sourceAccountId": "ID da Conta de Origem", "destinationAccountId": "ID da Conta de Destino", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} está em formato inválido", "parameter invalid currency": "{parameter} está em formato inválido", "parameter invalid color": "{parameter} está em formato inválido", - "parameter invalid amount filter": "{parameter} está em formato inválido" + "parameter invalid amount filter": "{parameter} está em formato inválido", + "parameter invalid tag filter": "{parameter} está em formato inválido" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Detecção automática", "Miscellaneous": "Diversos", "Default": "Padrão", + "Included": "Included", + "Excluded": "Excluded", "Done": "Concluído", "Continue": "Continuar", "Previous": "Anterior", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Inverter Seleção nesta Página", "Select All Valid Items": "Selecionar Todos os Itens Válidos", "Select All Invalid Items": "Selecionar Todos os Itens Inválidos", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Voltar", "Load More": "Carregar Mais", "Export Results": "Exportar Resultados", @@ -1802,10 +1811,10 @@ "Destination Account": "Conta de Destino", "Transaction Tag": "Transaction Tag", "Without Tags": "Sem Tags", - "With Any Selected Tags": "Com Quaisquer Tags Selecionadas", - "With All Selected Tags": "Com Todas as Tags Selecionadas", - "Without Any Selected Tags": "Sem Quaisquer Tags Selecionadas", - "Without All Selected Tags": "Sem Todas as Tags Selecionadas", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Várias Tags", "Transaction Time": "Horário da Transação", "Scheduled Transaction Frequency": "Frequência da Transação Agendada", diff --git a/src/locales/ru.json b/src/locales/ru.json index bff38cff..3438cbf3 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1300,7 +1300,7 @@ "balanceTime": "Время баланса", "startTime": "Время начала", "endTime": "Время окончания", - "tagFilterType": "Тип фильтра по тегам", + "tagFilter": "Tag Filter", "amountFilter": "Фильтр по сумме", "sourceAccountId": "ID исходного счета", "destinationAccountId": "ID целевого счета", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} имеет неверный формат", "parameter invalid currency": "{parameter} имеет неверный формат", "parameter invalid color": "{parameter} имеет неверный формат", - "parameter invalid amount filter": "{parameter} имеет неверный формат" + "parameter invalid amount filter": "{parameter} имеет неверный формат", + "parameter invalid tag filter": "{parameter} имеет неверный формат" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Auto detect", "Miscellaneous": "Разное", "Default": "По умолчанию", + "Included": "Included", + "Excluded": "Excluded", "Done": "Готово", "Continue": "Продолжить", "Previous": "Предыдущий", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Инвертировать выбор на этой странице", "Select All Valid Items": "Выбрать все действительные элементы", "Select All Invalid Items": "Выбрать все недействительные элементы", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Назад", "Load More": "Загрузить еще", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "Целевой счет", "Transaction Tag": "Transaction Tag", "Without Tags": "Без тегов", - "With Any Selected Tags": "С любыми выбранными тегами", - "With All Selected Tags": "Со всеми выбранными тегами", - "Without Any Selected Tags": "Без любых выбранных тегов", - "Without All Selected Tags": "Без всех выбранных тегов", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Несколько тегов", "Transaction Time": "Время транзакции", "Scheduled Transaction Frequency": "Частота запланированных транзакций", diff --git a/src/locales/th.json b/src/locales/th.json index c011ae61..57217f0a 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1300,7 +1300,7 @@ "balanceTime": "เวลาแสดงยอด", "startTime": "เวลาเริ่ม", "endTime": "เวลาสิ้นสุด", - "tagFilterType": "ประเภทตัวกรองแท็ก", + "tagFilter": "Tag Filter", "amountFilter": "ตัวกรองจำนวนเงิน", "sourceAccountId": "รหัสบัญชีต้นทาง", "destinationAccountId": "รหัสบัญชีปลายทาง", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "รูปแบบของ {parameter} ไม่ถูกต้อง", "parameter invalid currency": "รูปแบบของ {parameter} ไม่ถูกต้อง", "parameter invalid color": "รูปแบบของ {parameter} ไม่ถูกต้อง", - "parameter invalid amount filter": "รูปแบบของ {parameter} ไม่ถูกต้อง" + "parameter invalid amount filter": "รูปแบบของ {parameter} ไม่ถูกต้อง", + "parameter invalid tag filter": "รูปแบบของ {parameter} ไม่ถูกต้อง" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "ตรวจสอบอัตโนมัติ", "Miscellaneous": "อื่น ๆ", "Default": "ค่าเริ่มต้น", + "Included": "Included", + "Excluded": "Excluded", "Done": "เสร็จสิ้น", "Continue": "ดำเนินการต่อ", "Previous": "ก่อนหน้า", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "สลับการเลือกในหน้านี้", "Select All Valid Items": "เลือกทุกไอเทมที่ถูกต้อง", "Select All Invalid Items": "เลือกทุกไอเทมที่ไม่ถูกต้อง", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "กลับ", "Load More": "โหลดเพิ่มเติม", "Export Results": "ส่งออกผลลัพธ์", @@ -1802,10 +1811,10 @@ "Destination Account": "บัญชีปลายทาง", "Transaction Tag": "แท็กรายการ", "Without Tags": "ไม่มีแท็ก", - "With Any Selected Tags": "มีแท็กที่เลือกใด ๆ", - "With All Selected Tags": "มีแท็กทั้งหมดที่เลือก", - "Without Any Selected Tags": "ไม่มีแท็กที่เลือกใด ๆ", - "Without All Selected Tags": "ไม่มีแท็กทั้งหมดที่เลือก", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "หลายแท็ก", "Transaction Time": "เวลาธุรกรรม", "Scheduled Transaction Frequency": "ความถี่รายการที่กำหนดเวลา", diff --git a/src/locales/uk.json b/src/locales/uk.json index 5e8c8312..3f113ee5 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1300,7 +1300,7 @@ "balanceTime": "Час балансу", "startTime": "Час початку", "endTime": "Час завершення", - "tagFilterType": "Тип фільтра за тегами", + "tagFilter": "Tag Filter", "amountFilter": "Фільтр за сумою", "sourceAccountId": "ID вихідного рахунку", "destinationAccountId": "ID цільового рахунку", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} має некоректний формат", "parameter invalid currency": "{parameter} має некоректний формат", "parameter invalid color": "{parameter} має некоректний формат", - "parameter invalid amount filter": "{parameter} має некоректний формат" + "parameter invalid amount filter": "{parameter} має некоректний формат", + "parameter invalid tag filter": "{parameter} має некоректний формат" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Автовизначення", "Miscellaneous": "Різне", "Default": "По замовчуванню", + "Included": "Included", + "Excluded": "Excluded", "Done": "Готово", "Continue": "Продовжити", "Previous": "Назад", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Інвертувати вибір на цій сторінці", "Select All Valid Items": "Вибрати всі дійсні елементи", "Select All Invalid Items": "Вибрати всі недійсні елементи", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Назад", "Load More": "Завантажити ще", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "Цільовий рахунок", "Transaction Tag": "Transaction Tag", "Without Tags": "Без тегів", - "With Any Selected Tags": "З будь-якими вибраними тегами", - "With All Selected Tags": "З усіма вибраними тегами", - "Without Any Selected Tags": "Без будь-якого з вибраних тегів", - "Without All Selected Tags": "Без усіх вибраних тегів", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude All Selected Tags", "Multiple Tags": "Кілька тегів", "Transaction Time": "Час транзакції", "Scheduled Transaction Frequency": "Частота запланованої транзакції", diff --git a/src/locales/vi.json b/src/locales/vi.json index 444b587b..35440f88 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1300,7 +1300,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", + "tagFilter": "Tag Filter", "amountFilter": "Bộ lọc số tiền", "sourceAccountId": "ID tài khoản nguồn", "destinationAccountId": "ID tài khoản đích", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter} có định dạng không hợp lệ", "parameter invalid currency": "{parameter} có định dạng không hợp lệ", "parameter invalid color": "{parameter} có định dạng không hợp lệ", - "parameter invalid amount filter": "{parameter} có định dạng không hợp lệ" + "parameter invalid amount filter": "{parameter} có định dạng không hợp lệ", + "parameter invalid tag filter": "{parameter} có định dạng không hợp lệ" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "Auto detect", "Miscellaneous": "Linh tinh", "Default": "Mặc định", + "Included": "Included", + "Excluded": "Excluded", "Done": "Hoàn tất", "Continue": "Tiếp tục", "Previous": "Trước", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "Đảo ngược lựa chọn trong trang này", "Select All Valid Items": "Chọn tất cả các mục hợp lệ", "Select All Invalid Items": "Chọn tất cả các mục không hợp lệ", + "Set All to Included": "Set All to Included", + "Set All to Default": "Set All to Default", + "Set All to Excluded": "Set All to Excluded", + "Set All Visible Items to Included": "Set All Visible Items to Included", + "Set All Visible Items to Default": "Set All Visible Items to Default", + "Set All Visible Items to Excluded": "Set All Visible Items to Excluded", "Back": "Quay lại", "Load More": "Tải thêm", "Export Results": "Export Results", @@ -1802,10 +1811,10 @@ "Destination Account": "Tài khoản đích", "Transaction Tag": "Transaction Tag", "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", + "Include Any Selected Tags": "Include Any Selected Tags", + "Include All Selected Tags": "Include All Selected Tags", + "Exclude Any Selected Tags": "Exclude Any Selected Tags", + "Exclude All Selected Tags": "Exclude 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 9ff98c6f..edde0519 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1300,7 +1300,7 @@ "balanceTime": "余额时间", "startTime": "开始时间", "endTime": "结束时间", - "tagFilterType": "标签过滤类型", + "tagFilter": "标签过滤", "amountFilter": "金额过滤", "sourceAccountId": "来源账户ID", "destinationAccountId": "目标账户ID", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter}格式错误", "parameter invalid currency": "{parameter}格式错误", "parameter invalid color": "{parameter}格式错误", - "parameter invalid amount filter": "{parameter}格式错误" + "parameter invalid amount filter": "{parameter}格式错误", + "parameter invalid tag filter": "{parameter}格式错误" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "自动检测", "Miscellaneous": "杂项", "Default": "默认", + "Included": "包含", + "Excluded": "排除", "Done": "完成", "Continue": "继续", "Previous": "上一步", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "本页反选", "Select All Valid Items": "选择全部有效项目", "Select All Invalid Items": "选择全部无效项目", + "Set All to Included": "全部设置为包含", + "Set All to Default": "全部设置为默认", + "Set All to Excluded": "全部设置为排除", + "Set All Visible Items to Included": "全部可见项目设置为包含", + "Set All Visible Items to Default": "全部可见项目设置为默认", + "Set All Visible Items to Excluded": "全部可见项目设置为排除", "Back": "返回", "Load More": "加载更多", "Export Results": "导出结果", @@ -1802,10 +1811,10 @@ "Destination Account": "目标账户", "Transaction Tag": "交易标签", "Without Tags": "没有标签", - "With Any Selected Tags": "包含任意选中的标签", - "With All Selected Tags": "包含全部选中的标签", - "Without Any Selected Tags": "不包含任意选中的标签", - "Without All Selected Tags": "不包含全部选中的标签", + "Include Any Selected Tags": "包含任意已选标签", + "Include All Selected Tags": "包含所有已选标签", + "Exclude Any Selected Tags": "排除任意已选标签", + "Exclude All Selected Tags": "排除所有已选标签", "Multiple Tags": "多个标签", "Transaction Time": "交易时间", "Scheduled Transaction Frequency": "定时交易周期", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 3eeb3fdc..11856636 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1300,7 +1300,7 @@ "balanceTime": "餘額時間", "startTime": "開始時間", "endTime": "結束時間", - "tagFilterType": "標籤篩選類型", + "tagFilter": "標籤篩選", "amountFilter": "金額篩選", "sourceAccountId": "來源帳戶ID", "destinationAccountId": "目標帳戶ID", @@ -1328,7 +1328,8 @@ "parameter invalid email format": "{parameter}格式錯誤", "parameter invalid currency": "{parameter}格式錯誤", "parameter invalid color": "{parameter}格式錯誤", - "parameter invalid amount filter": "{parameter}格式錯誤" + "parameter invalid amount filter": "{parameter}格式錯誤", + "parameter invalid tag filter": "{parameter}格式錯誤" }, "encoding": { "utf-8": "UTF-8", @@ -1442,6 +1443,8 @@ "Auto detect": "自動偵測", "Miscellaneous": "雜項", "Default": "預設", + "Included": "包含", + "Excluded": "排除", "Done": "完成", "Continue": "繼續", "Previous": "上一步", @@ -1549,6 +1552,12 @@ "Invert Selection in This Page": "本頁反向選擇", "Select All Valid Items": "選擇全部有效項目", "Select All Invalid Items": "選擇全部無效項目", + "Set All to Included": "全部設為包含", + "Set All to Default": "全部設為預設", + "Set All to Excluded": "全部設為排除", + "Set All Visible Items to Included": "全部可見項目設為包含", + "Set All Visible Items to Default": "全部可見項目設為預設", + "Set All Visible Items to Excluded": "全部可見項目設為排除", "Back": "返回", "Load More": "載入更多", "Export Results": "匯出結果", @@ -1802,10 +1811,10 @@ "Destination Account": "目標帳戶", "Transaction Tag": "交易標籤", "Without Tags": "沒有標籤", - "With Any Selected Tags": "包含任意選中的標籤", - "With All Selected Tags": "包含全部選中的標籤", - "Without Any Selected Tags": "不包含任意選中的標籤", - "Without All Selected Tags": "不包含全部選中的標籤", + "Include Any Selected Tags": "包含任一選取的標籤", + "Include All Selected Tags": "包含所有選取的標籤", + "Exclude Any Selected Tags": "排除任一選取的標籤", + "Exclude All Selected Tags": "排除所有選取的標籤", "Multiple Tags": "多個標籤", "Transaction Time": "交易時間", "Scheduled Transaction Frequency": "排程交易週期", diff --git a/src/models/data_management.ts b/src/models/data_management.ts index 1a0e3426..c204ea12 100644 --- a/src/models/data_management.ts +++ b/src/models/data_management.ts @@ -4,8 +4,7 @@ export interface ExportTransactionDataRequest { readonly type: number; readonly categoryIds: string; readonly accountIds: string; - readonly tagIds: string; - readonly tagFilterType: number; + readonly tagFilter: string; readonly amountFilter: string; readonly keyword: string; } diff --git a/src/models/transaction.ts b/src/models/transaction.ts index f05a1bbf..0ba42b5f 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -1,7 +1,7 @@ import { type PartialRecord, itemAndIndex } from '@/core/base.ts'; import type { TextualYearMonthDay, Year1BasedMonth, YearMonthDay, StartEndTime, WeekDay } from '@/core/datetime.ts'; import { type Coordinate, getNormalizedCoordinate } from '@/core/coordinate.ts'; -import { TransactionType } from '@/core/transaction.ts'; +import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts'; import { Account, type AccountInfoResponse } from './account.ts'; import { TransactionCategory, type TransactionCategoryInfoResponse } from './transaction_category.ts'; @@ -437,6 +437,76 @@ export class TransactionGeoLocation implements TransactionGeoLocationRequest { } } +export class TransactionTagFilter { + public readonly tagIds: string[] + public readonly type: TransactionTagFilterType; + + public static readonly TransactionNoTagFilterValue: string = 'none'; + + private constructor(tagIds: string[], type: TransactionTagFilterType) { + this.tagIds = tagIds; + this.type = type; + } + + public static create(type: TransactionTagFilterType): TransactionTagFilter { + return new TransactionTagFilter([], type); + } + + public static of(tagId: string): TransactionTagFilter { + return new TransactionTagFilter([tagId], TransactionTagFilterType.HasAny); + } + + public static parse(tagFilter: string): TransactionTagFilter[] { + const ret: TransactionTagFilter[] = []; + + if (!tagFilter || tagFilter === TransactionTagFilter.TransactionNoTagFilterValue) { + return ret; + } + + const filters: string[] = tagFilter.split(';'); + + for (const filter of filters) { + const tagFilterItem: string[] = filter.split(':'); + + if (tagFilterItem.length !== 2) { + continue; + } + + const tagFilterTypeValue: number = parseInt(tagFilterItem[0] as string, 10); + + if (Number.isNaN(tagFilterTypeValue) || !Number.isFinite(tagFilterTypeValue)) { + continue; + } + + const tagFilterType: TransactionTagFilterType | undefined = TransactionTagFilterType.parse(tagFilterTypeValue); + + if (!tagFilterType) { + continue; + } + + const tagIds: string[] = (tagFilterItem[1] as string).split(','); + const tagFilter: TransactionTagFilter = new TransactionTagFilter(tagIds, tagFilterType); + ret.push(tagFilter); + } + + return ret; + } + + public static toTextualTagFilters(tagFilters: TransactionTagFilter[]): string { + const textualTagFilters: string[] = []; + + for (const tagFilter of tagFilters) { + textualTagFilters.push(tagFilter.toTextualTagFilter()); + } + + return textualTagFilters.join(';'); + } + + public toTextualTagFilter(): string { + return `${this.type.type}:${this.tagIds.join(',')}`; + } +} + export interface TransactionDraft { readonly type?: number; readonly categoryId?: string; @@ -511,8 +581,7 @@ export interface TransactionListByMaxTimeRequest { readonly type: number; readonly categoryIds: string; readonly accountIds: string; - readonly tagIds: string; - readonly tagFilterType: number; + readonly tagFilter: string; readonly amountFilter: string; readonly keyword: string; } @@ -523,8 +592,7 @@ export interface TransactionListInMonthByPageRequest { readonly type: number; readonly categoryIds: string; readonly accountIds: string; - readonly tagIds: string; - readonly tagFilterType: number; + readonly tagFilter: string; readonly amountFilter: string; readonly keyword: string; } @@ -563,8 +631,7 @@ export interface TransactionInfoResponse { export interface TransactionStatisticRequest { readonly startTime: number; readonly endTime: number; - readonly tagIds: string; - readonly tagFilterType: number; + readonly tagFilter: string; readonly keyword: string; readonly useTransactionTimezone: boolean; } @@ -575,8 +642,7 @@ export interface YearMonthRangeRequest { } export interface TransactionStatisticTrendsRequest extends YearMonthRangeRequest { - readonly tagIds: string; - readonly tagFilterType: number; + readonly tagFilter: string; readonly keyword: string; readonly useTransactionTimezone: boolean; } diff --git a/src/router/desktop.ts b/src/router/desktop.ts index 898c9a79..3425515e 100644 --- a/src/router/desktop.ts +++ b/src/router/desktop.ts @@ -111,8 +111,7 @@ const router = createRouter({ initType: route.query['type'], initCategoryIds: route.query['categoryIds'], initAccountIds: route.query['accountIds'], - initTagIds: route.query['tagIds'], - initTagFilterType: route.query['tagFilterType'], + initTagFilter: route.query['tagFilter'], initAmountFilter: route.query['amountFilter'], initKeyword: route.query['keyword'] }) @@ -130,8 +129,7 @@ const router = createRouter({ initEndTime: route.query['endTime'], initFilterAccountIds: route.query['filterAccountIds'], initFilterCategoryIds: route.query['filterCategoryIds'], - initTagIds: route.query['tagIds'], - initTagFilterType: route.query['tagFilterType'], + initTagFilter: route.query['tagFilter'], initKeyword: route.query['keyword'], initSortingType: route.query['sortingType'], initTrendDateAggregationType: route.query['trendDateAggregationType'], diff --git a/src/stores/statistics.ts b/src/stores/statistics.ts index 508e647e..b9706a33 100644 --- a/src/stores/statistics.ts +++ b/src/stores/statistics.ts @@ -12,8 +12,7 @@ import { type DateTime, type TextualYearMonth, type TimeRangeAndDateType, DateRa import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { CategoryType } from '@/core/category.ts'; import { - TransactionRelatedAccountType, - TransactionTagFilterType + TransactionRelatedAccountType } from '@/core/transaction.ts'; import { StatisticsAnalysisType, @@ -135,8 +134,7 @@ export interface TransactionStatisticsPartialFilter { assetTrendsChartEndTime?: number; filterAccountIds?: Record; filterCategoryIds?: Record; - tagIds?: string; - tagFilterType?: number; + tagFilter?: string; keyword?: string; sortingType?: number; } @@ -157,8 +155,7 @@ export interface TransactionStatisticsFilter extends TransactionStatisticsPartia assetTrendsChartEndTime: number; filterAccountIds: Record; filterCategoryIds: Record; - tagIds: string; - tagFilterType: number; + tagFilter: string; keyword: string; sortingType: number; } @@ -186,8 +183,7 @@ export const useStatisticsStore = defineStore('statistics', () => { assetTrendsChartEndTime: 0, filterAccountIds: {}, filterCategoryIds: {}, - tagIds: '', - tagFilterType: TransactionTagFilterType.Default.type, + tagFilter: '', keyword: '', sortingType: ChartSortingType.Default.type }); @@ -1326,8 +1322,7 @@ export const useStatisticsStore = defineStore('statistics', () => { transactionStatisticsFilter.value.assetTrendsChartEndTime = 0; transactionStatisticsFilter.value.filterAccountIds = {}; transactionStatisticsFilter.value.filterCategoryIds = {}; - transactionStatisticsFilter.value.tagIds = ''; - transactionStatisticsFilter.value.tagFilterType = TransactionTagFilterType.Default.type; + transactionStatisticsFilter.value.tagFilter = ''; transactionStatisticsFilter.value.keyword = ''; transactionCategoryStatisticsData.value = null; transactionCategoryTrendsData.value = []; @@ -1502,16 +1497,10 @@ export const useStatisticsStore = defineStore('statistics', () => { transactionStatisticsFilter.value.filterCategoryIds = settingsStore.appSettings.statistics.defaultTransactionCategoryFilter || {}; } - if (filter && isString(filter.tagIds)) { - transactionStatisticsFilter.value.tagIds = filter.tagIds; + if (filter && isString(filter.tagFilter)) { + transactionStatisticsFilter.value.tagFilter = filter.tagFilter; } else { - transactionStatisticsFilter.value.tagIds = ''; - } - - if (filter && isInteger(filter.tagFilterType)) { - transactionStatisticsFilter.value.tagFilterType = filter.tagFilterType; - } else { - transactionStatisticsFilter.value.tagFilterType = TransactionTagFilterType.Default.type; + transactionStatisticsFilter.value.tagFilter = ''; } if (filter && isString(filter.keyword)) { @@ -1613,13 +1602,8 @@ export const useStatisticsStore = defineStore('statistics', () => { changed = true; } - if (filter && isString(filter.tagIds) && transactionStatisticsFilter.value.tagIds !== filter.tagIds) { - transactionStatisticsFilter.value.tagIds = filter.tagIds; - changed = true; - } - - if (filter && isInteger(filter.tagFilterType) && transactionStatisticsFilter.value.tagFilterType !== filter.tagFilterType) { - transactionStatisticsFilter.value.tagFilterType = filter.tagFilterType; + if (filter && isString(filter.tagFilter) && transactionStatisticsFilter.value.tagFilter !== filter.tagFilter) { + transactionStatisticsFilter.value.tagFilter = filter.tagFilter; changed = true; } @@ -1692,12 +1676,8 @@ export const useStatisticsStore = defineStore('statistics', () => { } } - if (transactionStatisticsFilter.value.tagIds) { - querys.push('tagIds=' + transactionStatisticsFilter.value.tagIds); - } - - if (transactionStatisticsFilter.value.tagFilterType) { - querys.push('tagFilterType=' + transactionStatisticsFilter.value.tagFilterType); + if (transactionStatisticsFilter.value.tagFilter) { + querys.push('tagFilter=' + transactionStatisticsFilter.value.tagFilter); } if (transactionStatisticsFilter.value.keyword) { @@ -1798,12 +1778,8 @@ export const useStatisticsStore = defineStore('statistics', () => { } if (analysisType === StatisticsAnalysisType.CategoricalAnalysis || analysisType === StatisticsAnalysisType.TrendAnalysis) { - if (transactionStatisticsFilter.value.tagIds) { - querys.push('tagIds=' + transactionStatisticsFilter.value.tagIds); - } - - if (transactionStatisticsFilter.value.tagFilterType) { - querys.push('tagFilterType=' + transactionStatisticsFilter.value.tagFilterType); + if (transactionStatisticsFilter.value.tagFilter) { + querys.push('tagFilter=' + transactionStatisticsFilter.value.tagFilter); } if (transactionStatisticsFilter.value.keyword) { @@ -1834,8 +1810,7 @@ export const useStatisticsStore = defineStore('statistics', () => { services.getTransactionStatistics({ startTime: transactionStatisticsFilter.value.categoricalChartStartTime, endTime: transactionStatisticsFilter.value.categoricalChartEndTime, - tagIds: transactionStatisticsFilter.value.tagIds, - tagFilterType: transactionStatisticsFilter.value.tagFilterType, + tagFilter: transactionStatisticsFilter.value.tagFilter, keyword: transactionStatisticsFilter.value.keyword, useTransactionTimezone: settingsStore.appSettings.statistics.defaultTimezoneType === TimezoneTypeForStatistics.TransactionTimezone.type }).then(response => { @@ -1877,8 +1852,7 @@ export const useStatisticsStore = defineStore('statistics', () => { services.getTransactionStatisticsTrends({ startYearMonth: transactionStatisticsFilter.value.trendChartStartYearMonth, endYearMonth: transactionStatisticsFilter.value.trendChartEndYearMonth, - tagIds: transactionStatisticsFilter.value.tagIds, - tagFilterType: transactionStatisticsFilter.value.tagFilterType, + tagFilter: transactionStatisticsFilter.value.tagFilter, keyword: transactionStatisticsFilter.value.keyword, useTransactionTimezone: settingsStore.appSettings.statistics.defaultTimezoneType === TimezoneTypeForStatistics.TransactionTimezone.type }).then(response => { diff --git a/src/stores/transaction.ts b/src/stores/transaction.ts index 75c29686..d42e74ca 100644 --- a/src/stores/transaction.ts +++ b/src/stores/transaction.ts @@ -21,6 +21,7 @@ import { type TransactionPageWrapper, type TransactionReconciliationStatementResponse, Transaction, + TransactionTagFilter, EMPTY_TRANSACTION_RESULT } from '@/models/transaction.ts'; import type { @@ -47,6 +48,7 @@ import { isNumber, isString, isArray1SubsetOfArray2, + getObjectOwnFieldCount, splitItemsToMap, countSplitItems } from '@/lib/common.ts'; @@ -69,8 +71,7 @@ export interface TransactionListPartialFilter { type?: number; categoryIds?: string; accountIds?: string; - tagIds?: string; - tagFilterType?: number; + tagFilter?: string; amountFilter?: string; keyword?: string; } @@ -82,8 +83,7 @@ export interface TransactionListFilter extends TransactionListPartialFilter { type: number; categoryIds: string; accountIds: string; - tagIds: string; - tagFilterType: number; + tagFilter: string; amountFilter: string; keyword: string; } @@ -123,8 +123,7 @@ export const useTransactionsStore = defineStore('transactions', () => { type: 0, categoryIds: '', accountIds: '', - tagIds: '', - tagFilterType: TransactionTagFilterType.Default.type, + tagFilter: '', amountFilter: '', keyword: '' }); @@ -136,11 +135,32 @@ export const useTransactionsStore = defineStore('transactions', () => { const allFilterCategoryIds = computed>(() => splitItemsToMap(transactionsFilter.value.categoryIds, ',')); const allFilterAccountIds = computed>(() => splitItemsToMap(transactionsFilter.value.accountIds, ',')); - const allFilterTagIds = computed>(() => splitItemsToMap(transactionsFilter.value.tagIds, ',')); + const allFilterTagIds = computed>(() => { + const tagFilters: TransactionTagFilter[] = TransactionTagFilter.parse(transactionsFilter.value.tagFilter); + const allTagIdsMap: Record = {}; + + for (const tagFilter of tagFilters) { + let state: boolean = true; + + if (tagFilter.type === TransactionTagFilterType.HasAny || tagFilter.type === TransactionTagFilterType.HasAll) { + state = true; + } else if (tagFilter.type === TransactionTagFilterType.NotHasAny || tagFilter.type === TransactionTagFilterType.NotHasAll) { + state = false; + } else { + continue; + } + + for (const tagId of tagFilter.tagIds) { + allTagIdsMap[tagId] = state; + } + } + + return allTagIdsMap; + }); const allFilterCategoryIdsCount = computed(() => countSplitItems(transactionsFilter.value.categoryIds, ',')); const allFilterAccountIdsCount = computed(() => countSplitItems(transactionsFilter.value.accountIds, ',')); - const allFilterTagIdsCount = computed(() => countSplitItems(transactionsFilter.value.tagIds, ',')); + const allFilterTagIdsCount = computed(() => getObjectOwnFieldCount(allFilterTagIds.value)); const noTransaction = computed(() => { for (const transactionMonthList of transactions.value) { @@ -587,8 +607,7 @@ export const useTransactionsStore = defineStore('transactions', () => { transactionsFilter.value.type = 0; transactionsFilter.value.categoryIds = ''; transactionsFilter.value.accountIds = ''; - transactionsFilter.value.tagIds = ''; - transactionsFilter.value.tagFilterType = TransactionTagFilterType.Default.type; + transactionsFilter.value.tagFilter = ''; transactionsFilter.value.amountFilter = ''; transactionsFilter.value.keyword = ''; transactions.value = []; @@ -640,16 +659,10 @@ export const useTransactionsStore = defineStore('transactions', () => { transactionsFilter.value.accountIds = ''; } - if (filter && isString(filter.tagIds)) { - transactionsFilter.value.tagIds = filter.tagIds; + if (filter && isString(filter.tagFilter)) { + transactionsFilter.value.tagFilter = filter.tagFilter; } else { - transactionsFilter.value.tagIds = ''; - } - - if (filter && isNumber(filter.tagFilterType)) { - transactionsFilter.value.tagFilterType = filter.tagFilterType; - } else { - transactionsFilter.value.tagFilterType = TransactionTagFilterType.Default.type; + transactionsFilter.value.tagFilter = ''; } if (filter && isString(filter.amountFilter)) { @@ -703,13 +716,8 @@ export const useTransactionsStore = defineStore('transactions', () => { changed = true; } - if (filter && isString(filter.tagIds) && transactionsFilter.value.tagIds !== filter.tagIds) { - transactionsFilter.value.tagIds = filter.tagIds; - changed = true; - } - - if (filter && isNumber(filter.tagFilterType) && transactionsFilter.value.tagFilterType !== filter.tagFilterType) { - transactionsFilter.value.tagFilterType = filter.tagFilterType; + if (filter && isString(filter.tagFilter) && transactionsFilter.value.tagFilter !== filter.tagFilter) { + transactionsFilter.value.tagFilter = filter.tagFilter; changed = true; } @@ -743,12 +751,8 @@ export const useTransactionsStore = defineStore('transactions', () => { querys.push('categoryIds=' + transactionsFilter.value.categoryIds); } - if (transactionsFilter.value.tagIds) { - querys.push('tagIds=' + transactionsFilter.value.tagIds); - } - - if (transactionsFilter.value.tagFilterType) { - querys.push('tagFilterType=' + transactionsFilter.value.tagFilterType); + if (transactionsFilter.value.tagFilter) { + querys.push('tagFilter=' + transactionsFilter.value.tagFilter); } querys.push('dateType=' + transactionsFilter.value.dateType); @@ -776,8 +780,7 @@ export const useTransactionsStore = defineStore('transactions', () => { type: transactionsFilter.value.type, categoryIds: transactionsFilter.value.categoryIds, accountIds: transactionsFilter.value.accountIds, - tagIds: transactionsFilter.value.tagIds, - tagFilterType: transactionsFilter.value.tagFilterType, + tagFilter: transactionsFilter.value.tagFilter, amountFilter: transactionsFilter.value.amountFilter, keyword: transactionsFilter.value.keyword }; @@ -802,8 +805,7 @@ export const useTransactionsStore = defineStore('transactions', () => { type: transactionsFilter.value.type, categoryIds: transactionsFilter.value.categoryIds, accountIds: transactionsFilter.value.accountIds, - tagIds: transactionsFilter.value.tagIds, - tagFilterType: transactionsFilter.value.tagFilterType, + tagFilter: transactionsFilter.value.tagFilter, amountFilter: transactionsFilter.value.amountFilter, keyword: transactionsFilter.value.keyword }).then(response => { @@ -882,8 +884,7 @@ export const useTransactionsStore = defineStore('transactions', () => { type: transactionsFilter.value.type, categoryIds: transactionsFilter.value.categoryIds, accountIds: transactionsFilter.value.accountIds, - tagIds: transactionsFilter.value.tagIds, - tagFilterType: transactionsFilter.value.tagFilterType, + tagFilter: transactionsFilter.value.tagFilter, amountFilter: transactionsFilter.value.amountFilter, keyword: transactionsFilter.value.keyword }).then(response => { diff --git a/src/styles/desktop/global.scss b/src/styles/desktop/global.scss index adf26cb6..c2af0c40 100644 --- a/src/styles/desktop/global.scss +++ b/src/styles/desktop/global.scss @@ -259,6 +259,33 @@ html[dir="rtl"] .bidirectional-switch { } } +.toggle-buttons { + &.v-btn-toggle { + height: auto !important; + padding: 0 !important; + border: none !important; + } + + &.v-btn-toggle > .v-btn:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: none; + } + + &.v-btn-toggle > .v-btn:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &.v-btn-toggle > .v-btn { + border: thin solid rgba(var(--v-border-color), var(--v-border-opacity)); + } + + &.v-btn-toggle button.v-btn { + width: auto !important; + } +} + .v-theme--dark { .v-btn--variant-elevated, .v-btn--variant-flat { diff --git a/src/views/base/settings/TransactionTagFilterSettingPageBase.ts b/src/views/base/settings/TransactionTagFilterSettingPageBase.ts index d42a33d6..bc1cc934 100644 --- a/src/views/base/settings/TransactionTagFilterSettingPageBase.ts +++ b/src/views/base/settings/TransactionTagFilterSettingPageBase.ts @@ -1,26 +1,35 @@ import { ref, computed } from 'vue'; -import { useI18n } from '@/locales/helpers.ts'; - import { useTransactionTagsStore } from '@/stores/transactionTag.ts'; import { useTransactionsStore } from '@/stores/transaction.ts'; import { useStatisticsStore } from '@/stores/statistics.ts'; -import { type TypeAndDisplayName, keys, keysIfValueEquals, values } from '@/core/base.ts'; +import { entries, values } from '@/core/base.ts'; import { TransactionTagFilterType } from '@/core/transaction.ts'; import type { TransactionTag } from '@/models/transaction_tag.ts'; +import { TransactionTagFilter } from '@/models/transaction.ts'; + +import { objectFieldWithValueToArrayItem } from '@/lib/common.ts'; + +export enum TransactionTagFilterState { + Default = 0, + Include = 1, + Exclude = 2 +} export function useTransactionTagFilterSettingPageBase(type?: string) { - const { getAllTransactionTagFilterTypes } = useI18n(); - const transactionTagsStore = useTransactionTagsStore(); const transactionsStore = useTransactionsStore(); const statisticsStore = useStatisticsStore(); const loading = ref(true); const showHidden = ref(false); - const filterTagIds = ref>({}); - const tagFilterType = ref(TransactionTagFilterType.Default.type); + const filterTagIds = ref>({}); + const includeTagFilterType = ref(TransactionTagFilterType.HasAny.type); + const excludeTagFilterType = ref(TransactionTagFilterType.NotHasAny.type); + + const includeTagsCount = computed(() => objectFieldWithValueToArrayItem(filterTagIds.value, TransactionTagFilterState.Include).length); + const excludeTagsCount = computed(() => objectFieldWithValueToArrayItem(filterTagIds.value, TransactionTagFilterState.Exclude).length); const title = computed(() => { return 'Filter Transaction Tags'; @@ -31,7 +40,6 @@ export function useTransactionTagFilterSettingPageBase(type?: string) { }); const allTags = computed(() => transactionTagsStore.allTransactionTags); - const allTagFilterTypes = computed(() => getAllTransactionTagFilterTypes()); const hasAnyAvailableTag = computed(() => transactionTagsStore.allAvailableTagsCount > 0); const hasAnyVisibleTag = computed(() => { if (showHidden.value) { @@ -42,67 +50,76 @@ export function useTransactionTagFilterSettingPageBase(type?: string) { }); function loadFilterTagIds(): boolean { - const allTransactionTagIds: Record = {}; - - for (const transactionTag of values(transactionTagsStore.allTransactionTagsMap)) { - allTransactionTagIds[transactionTag.id] = true; - } + let tagFilters: TransactionTagFilter[] = []; if (type === 'statisticsCurrent') { - const transactionTagIds = statisticsStore.transactionStatisticsFilter.tagIds ? statisticsStore.transactionStatisticsFilter.tagIds.split(',') : []; - - for (const transactionTagId of transactionTagIds) { - const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId]; - - if (transactionTag) { - allTransactionTagIds[transactionTag.id] = false; - } - } - filterTagIds.value = allTransactionTagIds; - tagFilterType.value = statisticsStore.transactionStatisticsFilter.tagFilterType; - return true; + tagFilters = TransactionTagFilter.parse(statisticsStore.transactionStatisticsFilter.tagFilter); } else if (type === 'transactionListCurrent') { - for (const transactionTagId of keysIfValueEquals(transactionsStore.allFilterTagIds, true)) { - const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId]; - - if (transactionTag) { - allTransactionTagIds[transactionTag.id] = false; - } - } - filterTagIds.value = allTransactionTagIds; - return true; + tagFilters = TransactionTagFilter.parse(transactionsStore.transactionsFilter.tagFilter); } else { return false; } + + const allTagIdsMap: Record = {}; + + for (const transactionTag of values(transactionTagsStore.allTransactionTagsMap)) { + allTagIdsMap[transactionTag.id] = TransactionTagFilterState.Default; + } + + for (const tagFilter of tagFilters) { + let state: TransactionTagFilterState = TransactionTagFilterState.Default; + + if (tagFilter.type === TransactionTagFilterType.HasAny || tagFilter.type === TransactionTagFilterType.HasAll) { + state = TransactionTagFilterState.Include; + includeTagFilterType.value = tagFilter.type.type; + } else if (tagFilter.type === TransactionTagFilterType.NotHasAny || tagFilter.type === TransactionTagFilterType.NotHasAll) { + state = TransactionTagFilterState.Exclude; + excludeTagFilterType.value = tagFilter.type.type; + } else { + continue; + } + + for (const tagId of tagFilter.tagIds) { + allTagIdsMap[tagId] = state; + } + } + + filterTagIds.value = allTagIdsMap; + return true; } function saveFilterTagIds(): boolean { - const filteredTagIds: Record = {}; - let finalTagIds = ''; + const includeTagFilter: TransactionTagFilter = TransactionTagFilter.create(TransactionTagFilterType.parse(includeTagFilterType.value) ?? TransactionTagFilterType.HasAny); + const excludeTagFilter: TransactionTagFilter = TransactionTagFilter.create(TransactionTagFilterType.parse(excludeTagFilterType.value) ?? TransactionTagFilterType.NotHasAny); let changed = true; - for (const transactionTagId of keys(filterTagIds.value)) { + for (const [transactionTagId, state] of entries(filterTagIds.value)) { const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId]; if (!transactionTag) { continue; } - if (filterTagIds.value[transactionTag.id]) { - filteredTagIds[transactionTag.id] = true; - } else { - if (finalTagIds.length > 0) { - finalTagIds += ','; - } - - finalTagIds += transactionTag.id; + if (state === TransactionTagFilterState.Include) { + includeTagFilter.tagIds.push(transactionTag.id); + } else if (state === TransactionTagFilterState.Exclude) { + excludeTagFilter.tagIds.push(transactionTag.id); } } + const tagFilters: TransactionTagFilter[] = []; + + if (includeTagFilter.tagIds.length > 0) { + tagFilters.push(includeTagFilter); + } + + if (excludeTagFilter.tagIds.length > 0) { + tagFilters.push(excludeTagFilter); + } + if (type === 'statisticsCurrent') { changed = statisticsStore.updateTransactionStatisticsFilter({ - tagIds: finalTagIds, - tagFilterType: tagFilterType.value + tagFilter: TransactionTagFilter.toTextualTagFilters(tagFilters) }); if (changed) { @@ -110,7 +127,7 @@ export function useTransactionTagFilterSettingPageBase(type?: string) { } } else if (type === 'transactionListCurrent') { changed = transactionsStore.updateTransactionListFilter({ - tagIds: finalTagIds + tagFilter: TransactionTagFilter.toTextualTagFilters(tagFilters) }); if (changed) { @@ -126,12 +143,14 @@ export function useTransactionTagFilterSettingPageBase(type?: string) { loading, showHidden, filterTagIds, - tagFilterType, + includeTagFilterType, + excludeTagFilterType, // computed states + includeTagsCount, + excludeTagsCount, title, applyText, allTags, - allTagFilterTypes, hasAnyAvailableTag, hasAnyVisibleTag, // functions diff --git a/src/views/base/transactions/TransactionListPageBase.ts b/src/views/base/transactions/TransactionListPageBase.ts index 88b2c5ab..ebea6fe1 100644 --- a/src/views/base/transactions/TransactionListPageBase.ts +++ b/src/views/base/transactions/TransactionListPageBase.ts @@ -9,7 +9,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useTransactionTagsStore } from '@/stores/transactionTag.ts'; import { type TransactionListFilter, type TransactionMonthList, useTransactionsStore } from '@/stores/transaction.ts'; -import { type TypeAndName, entries } from '@/core/base.ts'; +import { type TypeAndName, keys, entries } from '@/core/base.ts'; import type { NumeralSystem } from '@/core/numeral.ts'; import { type TextualYearMonthDay, type Year0BasedMonth, type LocalizedDateRange, type WeekDayValue, DateRange, DateRangeScene } from '@/core/datetime.ts'; import { AccountType } from '@/core/account.ts'; @@ -19,7 +19,7 @@ import { DISPLAY_HIDDEN_AMOUNT, INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numera import type { Account } from '@/models/account.ts'; import type { TransactionCategory } from '@/models/transaction_category.ts'; import type { TransactionTag } from '@/models/transaction_tag.ts'; -import type { Transaction } from '@/models/transaction.ts'; +import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts'; import { getUtcOffsetByUtcOffsetMinutes, @@ -196,7 +196,7 @@ export function useTransactionListPageBase() { }); const queryTagName = computed(() => { - if (query.value.tagIds === 'none') { + if (query.value.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue) { return tt('Without Tags'); } @@ -204,7 +204,15 @@ export function useTransactionListPageBase() { return tt('Multiple Tags'); } - return allTransactionTags.value[query.value.tagIds]?.name || tt('Tags'); + for (const tagId of keys(queryAllFilterTagIds.value)) { + const tagName = allTransactionTags.value[tagId]?.name; + + if (tagName) { + return tagName; + } + } + + return tt('Tags'); }); const queryAmount = computed(() => { diff --git a/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue b/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue index eb6b7b69..82fb346f 100644 --- a/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue +++ b/src/views/desktop/common/cards/TransactionTagFilterSettingsCard.vue @@ -11,22 +11,30 @@ + @click="setAllToState(false, TransactionTagFilterState.Include)"> + @click="setAllToState(false, TransactionTagFilterState.Default)"> + @click="setAllToState(false, TransactionTagFilterState.Exclude)"> + @click="setAllToState(true, TransactionTagFilterState.Include)"> + + + @click="setAllToState(false, TransactionTagFilterState.Include)"> + @click="setAllToState(false, TransactionTagFilterState.Default)"> + @click="setAllToState(false, TransactionTagFilterState.Exclude)"> + @click="setAllToState(true, TransactionTagFilterState.Include)"> + + -
- - {{ filterType.displayName }} - +
+ + {{ tt(TransactionTagFilterType.HasAny.name) }} + {{ tt(TransactionTagFilterType.HasAll.name) }} + +
+ +
+ + {{ tt(TransactionTagFilterType.NotHasAny.name) }} + {{ tt(TransactionTagFilterType.NotHasAll.name) }} +
@@ -108,18 +133,23 @@ v-for="transactionTag in allTags"> + @@ -146,19 +176,16 @@ import SnackBar from '@/components/desktop/SnackBar.vue'; import { ref, useTemplateRef } from 'vue'; import { useI18n } from '@/locales/helpers.ts'; -import { useTransactionTagFilterSettingPageBase } from '@/views/base/settings/TransactionTagFilterSettingPageBase.ts'; +import { + useTransactionTagFilterSettingPageBase, + TransactionTagFilterState +} from '@/views/base/settings/TransactionTagFilterSettingPageBase.ts'; import { useTransactionTagsStore } from '@/stores/transactionTag.ts'; +import { TransactionTagFilterType } from '@/core/transaction.ts'; import type { TransactionTag } from '@/models/transaction_tag.ts'; -import { - selectAllVisible, - selectAll, - selectNone, - selectInvert -} from '@/lib/common.ts'; - import { mdiSelectAll, mdiSelect, @@ -166,7 +193,6 @@ import { mdiEyeOutline, mdiEyeOffOutline, mdiDotsVertical, - mdiCheck, mdiPound } from '@mdi/js'; @@ -188,11 +214,13 @@ const { loading, showHidden, filterTagIds, - tagFilterType, + includeTagFilterType, + excludeTagFilterType, + includeTagsCount, + excludeTagsCount, title, applyText, allTags, - allTagFilterTypes, hasAnyAvailableTag, hasAnyVisibleTag, loadFilterTagIds, @@ -223,40 +251,38 @@ function init(): void { }); } -function updateTransactionTagSelected(transactionTag: TransactionTag, value: boolean | null): void { - filterTagIds.value[transactionTag.id] = !value; +function updateTransactionTagState(transactionTag: TransactionTag, value: TransactionTagFilterState): void { + filterTagIds.value[transactionTag.id] = value; if (props.autoSave) { save(); } } -function selectAllTransactionTags(): void { - selectAll(filterTagIds.value, transactionTagsStore.allTransactionTagsMap); +function updateTransactionTagIncludeType(value: number): void { + includeTagFilterType.value = value; if (props.autoSave) { save(); } } -function selectNoneTransactionTags(): void { - selectNone(filterTagIds.value, transactionTagsStore.allTransactionTagsMap); +function updateTransactionTagExcludeType(value: number): void { + excludeTagFilterType.value = value; if (props.autoSave) { save(); } } -function selectInvertTransactionTags(): void { - selectInvert(filterTagIds.value, transactionTagsStore.allTransactionTagsMap); +function setAllToState(onlyVisible: boolean, value: TransactionTagFilterState): void { + for (const tag of allTags.value) { + if (onlyVisible && !showHidden.value && tag.hidden) { + continue; + } - if (props.autoSave) { - save(); + filterTagIds.value[tag.id] = value; } -} - -function selectAllVisibleTransactionTags(): void { - selectAllVisible(filterTagIds.value, transactionTagsStore.allTransactionTagsMap); if (props.autoSave) { save(); @@ -276,15 +302,9 @@ init(); diff --git a/src/views/mobile/settings/TransactionTagFilterSettingsPage.vue b/src/views/mobile/settings/TransactionTagFilterSettingsPage.vue index e40456a2..bfda7ffd 100644 --- a/src/views/mobile/settings/TransactionTagFilterSettingsPage.vue +++ b/src/views/mobile/settings/TransactionTagFilterSettingsPage.vue @@ -40,14 +40,27 @@ - + + :value="filterType.type" + :checked="includeTagFilterType === filterType.type" + v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]" + @change="includeTagFilterType = filterType.type" + v-if="includeTagsCount > 1"> + + + + + @@ -68,14 +81,15 @@ - + @click="currentTransactionTagId = transactionTag.id">