tag filter supports selecting both included and excluded tags simultaneously

This commit is contained in:
MaysWind
2025-11-24 02:12:44 +08:00
parent 45be96cf68
commit 6430a52027
45 changed files with 1151 additions and 706 deletions

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -27,8 +27,7 @@ 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"`
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

View File

@@ -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,13 +202,17 @@ 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"`
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
@@ -217,8 +224,7 @@ 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"`
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
@@ -239,8 +245,7 @@ type TransactionListInMonthByPageRequest 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"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
WithPictures bool `form:"with_pictures"`
@@ -260,8 +265,7 @@ type TransactionReconciliationStatementRequest struct {
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"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
Keyword string `form:"keyword"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
@@ -269,8 +273,7 @@ type TransactionStatisticRequest struct {
// 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"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
Keyword string `form:"keyword"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
@@ -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) {

View File

@@ -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",

View File

@@ -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,7 +2616,8 @@ 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 {
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 {
@@ -2627,28 +2628,40 @@ func (s *TransactionService) appendFilterTagIdsConditionToQuery(sess *xorm.Sessi
subQueryCondition = subQueryCondition.And(builder.Gte{"transaction_time": minTransactionTime})
}
if noTags {
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 {
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 tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ALL {
} 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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}]
}
];

View File

@@ -40,13 +40,12 @@ export class TransactionEditScopeType implements TypeAndName {
export class TransactionTagFilterType implements TypeAndName {
private static readonly allInstances: TransactionTagFilterType[] = [];
private static readonly allInstancesByType: Record<number, TransactionTagFilterType> = {};
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];
}
}

View File

@@ -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<ApiResponse<boolean>>('v1/accounts/sub_account/delete.json', req);
},
getTransactions: (req: TransactionListByMaxTimeRequest): ApiResponsePromise<TransactionInfoPageWrapperResponse> => {
const tagFilter = encodeURIComponent(req.tagFilter);
const amountFilter = encodeURIComponent(req.amountFilter);
const keyword = encodeURIComponent(req.keyword);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse>>(`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<ApiResponse<TransactionInfoPageWrapperResponse>>(`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<TransactionInfoPageWrapperResponse2> => {
const tagFilter = encodeURIComponent(req.tagFilter);
const amountFilter = encodeURIComponent(req.amountFilter);
const keyword = encodeURIComponent(req.keyword);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse2>>(`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<ApiResponse<TransactionInfoPageWrapperResponse2>>(`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<TransactionReconciliationStatementResponse> => {
return axios.get<ApiResponse<TransactionReconciliationStatementResponse>>(`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) {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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",

View File

@@ -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": "スケジュールされた取引の頻度",

View File

@@ -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": "예약 거래 빈도",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "Частота запланированных транзакций",

View File

@@ -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": "ความถี่รายการที่กำหนดเวลา",

View File

@@ -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": "Частота запланованої транзакції",

View File

@@ -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",

View File

@@ -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": "定时交易周期",

View File

@@ -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": "排程交易週期",

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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'],

View File

@@ -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<string, boolean>;
filterCategoryIds?: Record<string, boolean>;
tagIds?: string;
tagFilterType?: number;
tagFilter?: string;
keyword?: string;
sortingType?: number;
}
@@ -157,8 +155,7 @@ export interface TransactionStatisticsFilter extends TransactionStatisticsPartia
assetTrendsChartEndTime: number;
filterAccountIds: Record<string, boolean>;
filterCategoryIds: Record<string, boolean>;
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 => {

View File

@@ -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<Record<string, boolean>>(() => splitItemsToMap(transactionsFilter.value.categoryIds, ','));
const allFilterAccountIds = computed<Record<string, boolean>>(() => splitItemsToMap(transactionsFilter.value.accountIds, ','));
const allFilterTagIds = computed<Record<string, boolean>>(() => splitItemsToMap(transactionsFilter.value.tagIds, ','));
const allFilterTagIds = computed<Record<string, boolean>>(() => {
const tagFilters: TransactionTagFilter[] = TransactionTagFilter.parse(transactionsFilter.value.tagFilter);
const allTagIdsMap: Record<string, boolean> = {};
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<number>(() => countSplitItems(transactionsFilter.value.categoryIds, ','));
const allFilterAccountIdsCount = computed<number>(() => countSplitItems(transactionsFilter.value.accountIds, ','));
const allFilterTagIdsCount = computed<number>(() => countSplitItems(transactionsFilter.value.tagIds, ','));
const allFilterTagIdsCount = computed<number>(() => getObjectOwnFieldCount(allFilterTagIds.value));
const noTransaction = computed<boolean>(() => {
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 => {

View File

@@ -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 {

View File

@@ -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<boolean>(true);
const showHidden = ref<boolean>(false);
const filterTagIds = ref<Record<string, boolean>>({});
const tagFilterType = ref<number>(TransactionTagFilterType.Default.type);
const filterTagIds = ref<Record<string, TransactionTagFilterState>>({});
const includeTagFilterType = ref<number>(TransactionTagFilterType.HasAny.type);
const excludeTagFilterType = ref<number>(TransactionTagFilterType.NotHasAny.type);
const includeTagsCount = computed<number>(() => objectFieldWithValueToArrayItem(filterTagIds.value, TransactionTagFilterState.Include).length);
const excludeTagsCount = computed<number>(() => objectFieldWithValueToArrayItem(filterTagIds.value, TransactionTagFilterState.Exclude).length);
const title = computed<string>(() => {
return 'Filter Transaction Tags';
@@ -31,7 +40,6 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
});
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const allTagFilterTypes = computed<TypeAndDisplayName[]>(() => getAllTransactionTagFilterTypes());
const hasAnyAvailableTag = computed<boolean>(() => transactionTagsStore.allAvailableTagsCount > 0);
const hasAnyVisibleTag = computed<boolean>(() => {
if (showHidden.value) {
@@ -42,67 +50,76 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
});
function loadFilterTagIds(): boolean {
const allTransactionTagIds: Record<string, boolean> = {};
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<string, TransactionTagFilterState> = {};
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<string, boolean> = {};
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 += ',';
if (state === TransactionTagFilterState.Include) {
includeTagFilter.tagIds.push(transactionTag.id);
} else if (state === TransactionTagFilterState.Exclude) {
excludeTagFilter.tagIds.push(transactionTag.id);
}
}
finalTagIds += 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

View File

@@ -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<string>(() => {
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<string>(() => {

View File

@@ -11,22 +11,30 @@
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All')"
:title="tt('Set All to Included')"
:disabled="!hasAnyVisibleTag"
@click="selectAllTransactionTags"></v-list-item>
@click="setAllToState(false, TransactionTagFilterState.Include)"></v-list-item>
<v-list-item :prepend-icon="mdiSelect"
:title="tt('Select None')"
:title="tt('Set All to Default')"
:disabled="!hasAnyVisibleTag"
@click="selectNoneTransactionTags"></v-list-item>
@click="setAllToState(false, TransactionTagFilterState.Default)"></v-list-item>
<v-list-item :prepend-icon="mdiSelectInverse"
:title="tt('Invert Selection')"
:title="tt('Set All to Excluded')"
:disabled="!hasAnyVisibleTag"
@click="selectInvertTransactionTags"></v-list-item>
@click="setAllToState(false, TransactionTagFilterState.Exclude)"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All Visible')"
:title="tt('Set All Visible Items to Included')"
:disabled="!hasAnyVisibleTag"
@click="selectAllVisibleTransactionTags"></v-list-item>
@click="setAllToState(true, TransactionTagFilterState.Include)"></v-list-item>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Set All Visible Items to Default')"
:disabled="!hasAnyVisibleTag"
@click="setAllToState(true, TransactionTagFilterState.Default)"></v-list-item>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Set All Visible Items to Excluded')"
:disabled="!hasAnyVisibleTag"
@click="setAllToState(true, TransactionTagFilterState.Exclude)"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiEyeOutline"
:title="tt('Show Hidden Transaction Tags')"
@@ -47,22 +55,30 @@
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All')"
:title="tt('Set All to Included')"
:disabled="!hasAnyVisibleTag"
@click="selectAllTransactionTags"></v-list-item>
@click="setAllToState(false, TransactionTagFilterState.Include)"></v-list-item>
<v-list-item :prepend-icon="mdiSelect"
:title="tt('Select None')"
:title="tt('Set All to Default')"
:disabled="!hasAnyVisibleTag"
@click="selectNoneTransactionTags"></v-list-item>
@click="setAllToState(false, TransactionTagFilterState.Default)"></v-list-item>
<v-list-item :prepend-icon="mdiSelectInverse"
:title="tt('Invert Selection')"
:title="tt('Set All to Excluded')"
:disabled="!hasAnyVisibleTag"
@click="selectInvertTransactionTags"></v-list-item>
@click="setAllToState(false, TransactionTagFilterState.Exclude)"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Select All Visible')"
:title="tt('Set All Visible Items to Included')"
:disabled="!hasAnyVisibleTag"
@click="selectAllVisibleTransactionTags"></v-list-item>
@click="setAllToState(true, TransactionTagFilterState.Include)"></v-list-item>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Set All Visible Items to Default')"
:disabled="!hasAnyVisibleTag"
@click="setAllToState(true, TransactionTagFilterState.Default)"></v-list-item>
<v-list-item :prepend-icon="mdiSelectAll"
:title="tt('Set All Visible Items to Excluded')"
:disabled="!hasAnyVisibleTag"
@click="setAllToState(true, TransactionTagFilterState.Exclude)"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="mdiEyeOutline"
:title="tt('Show Hidden Transaction Tags')"
@@ -86,15 +102,24 @@
</v-card-text>
<v-card-text :class="{ 'mt-0 mt-sm-2 mt-md-4': dialogMode }" v-else-if="!loading && hasAnyVisibleTag">
<div class="tag-filter-types d-flex flex-column mb-4" v-if="type === 'statisticsCurrent'">
<v-btn border class="justify-start" :key="filterType.type"
:color="tagFilterType === filterType.type ? 'primary' : 'default'"
:variant="tagFilterType === filterType.type ? 'tonal' : 'outlined'"
:append-icon="(tagFilterType === filterType.type ? mdiCheck : undefined)"
v-for="filterType in allTagFilterTypes"
@click="tagFilterType = filterType.type">
{{ filterType.displayName }}
</v-btn>
<div class="mb-4" v-if="includeTagsCount > 1">
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
mandatory="force" divided
:model-value="includeTagFilterType"
@update:model-value="updateTransactionTagIncludeType($event)">
<v-btn :value="TransactionTagFilterType.HasAny.type">{{ tt(TransactionTagFilterType.HasAny.name) }}</v-btn>
<v-btn :value="TransactionTagFilterType.HasAll.type">{{ tt(TransactionTagFilterType.HasAll.name) }}</v-btn>
</v-btn-toggle>
</div>
<div class="mb-4" v-if="excludeTagsCount > 1">
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
mandatory="force" divided
:model-value="excludeTagFilterType"
@update:model-value="updateTransactionTagExcludeType($event)">
<v-btn :value="TransactionTagFilterType.NotHasAny.type">{{ tt(TransactionTagFilterType.NotHasAny.name) }}</v-btn>
<v-btn :value="TransactionTagFilterType.NotHasAll.type">{{ tt(TransactionTagFilterType.NotHasAll.name) }}</v-btn>
</v-btn-toggle>
</div>
<v-expansion-panels class="tag-categories" multiple v-model="expandTagCategories">
@@ -108,9 +133,6 @@
v-for="transactionTag in allTags">
<v-list-item v-if="showHidden || !transactionTag.hidden">
<template #prepend>
<v-checkbox :model-value="!filterTagIds[transactionTag.id]"
@update:model-value="updateTransactionTagSelected(transactionTag, $event)">
<template #label>
<v-badge class="right-bottom-icon" color="secondary"
location="bottom right" offset-x="2" offset-y="2" :icon="mdiEyeOffOutline"
v-if="transactionTag.hidden">
@@ -119,7 +141,15 @@
<v-icon size="24" :icon="mdiPound" v-else-if="!transactionTag.hidden"/>
<span class="ms-3">{{ transactionTag.name }}</span>
</template>
</v-checkbox>
<template #append>
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
mandatory="force" divided
:model-value="filterTagIds[transactionTag.id]"
@update:model-value="updateTransactionTagState(transactionTag, $event)">
<v-btn :value="TransactionTagFilterState.Include">{{ tt('Included') }}</v-btn>
<v-btn :value="TransactionTagFilterState.Default">{{ tt('Default') }}</v-btn>
<v-btn :value="TransactionTagFilterState.Exclude">{{ tt('Excluded') }}</v-btn>
</v-btn-toggle>
</template>
</v-list-item>
</template>
@@ -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);
if (props.autoSave) {
save();
function setAllToState(onlyVisible: boolean, value: TransactionTagFilterState): void {
for (const tag of allTags.value) {
if (onlyVisible && !showHidden.value && tag.hidden) {
continue;
}
}
function selectAllVisibleTransactionTags(): void {
selectAllVisible(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
filterTagIds.value[tag.id] = value;
}
if (props.autoSave) {
save();
@@ -276,15 +302,9 @@ init();
</script>
<style>
.tag-filter-types .v-btn:not(:first-child) {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.tag-filter-types .v-btn:not(:last-child) {
border-bottom: 0;
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
.tag-categories .tag-filter-state-toggle {
overflow-x: auto;
white-space: nowrap;
}
.tag-categories .v-expansion-panel-text__wrapper {

View File

@@ -576,8 +576,7 @@ interface TransactionStatisticsProps {
initEndTime?: TextualYearMonth | '',
initFilterAccountIds?: string,
initFilterCategoryIds?: string,
initTagIds?: string,
initTagFilterType?: string,
initTagFilter?: string,
initKeyword?: string;
initSortingType?: string,
initTrendDateAggregationType?: string
@@ -757,8 +756,7 @@ function init(initProps: TransactionStatisticsProps): void {
chartDataType: initProps.initChartDataType ? parseInt(initProps.initChartDataType) : undefined,
filterAccountIds: initProps.initFilterAccountIds ? arrayItemToObjectField(initProps.initFilterAccountIds.split(','), true) : {},
filterCategoryIds: initProps.initFilterCategoryIds ? arrayItemToObjectField(initProps.initFilterCategoryIds.split(','), true) : {},
tagIds: initProps.initTagIds,
tagFilterType: initProps.initTagFilterType && parseInt(initProps.initTagFilterType) >= 0 ? parseInt(initProps.initTagFilterType) : undefined,
tagFilter: initProps.initTagFilter,
keyword: initProps.initKeyword,
sortingType: initProps.initSortingType ? parseInt(initProps.initSortingType) : undefined
};
@@ -1314,8 +1312,7 @@ onBeforeRouteUpdate((to) => {
initEndTime: (to.query['endTime'] as TextualYearMonth | null) || undefined,
initFilterAccountIds: (to.query['filterAccountIds'] as string | null) || undefined,
initFilterCategoryIds: (to.query['filterCategoryIds'] as string | null) || undefined,
initTagIds: (to.query['tagIds'] as string | null) || undefined,
initTagFilterType: (to.query['tagFilterType'] as string | null) || undefined,
initTagFilter: (to.query['tagFilter'] as string | null) || undefined,
initKeyword: (to.query['keyword'] as string | null) || undefined,
initSortingType: (to.query['sortingType'] as string | null) || undefined,
initTrendDateAggregationType: (to.query['trendDateAggregationType'] as string | null) || undefined,

View File

@@ -431,15 +431,15 @@
@update:model-value="scrollTagMenuToSelectedItem">
<template #activator="{ props }">
<div class="d-flex align-center cursor-pointer"
:class="{ 'readonly': loading, 'text-primary': query.tagIds }" v-bind="props">
:class="{ 'readonly': loading, 'text-primary': query.tagFilter }" v-bind="props">
<span>{{ queryTagName }}</span>
<v-icon :icon="mdiMenuDown" />
</div>
</template>
<v-list :selected="[queryAllSelectedFilterTagIds]">
<v-list-item key="" value="" class="text-sm" density="compact"
:class="{ 'list-item-selected': !query.tagIds }"
:append-icon="(!query.tagIds ? mdiCheck : undefined)">
:class="{ 'list-item-selected': !query.tagFilter }"
:append-icon="(!query.tagFilter ? mdiCheck : undefined)">
<v-list-item-title class="cursor-pointer"
@click="changeTagFilter('')">
<div class="d-flex align-center">
@@ -448,11 +448,13 @@
</div>
</v-list-item-title>
</v-list-item>
<v-list-item key="none" value="none" class="text-sm" density="compact"
:class="{ 'list-item-selected': query.tagIds === 'none' }"
:append-icon="(query.tagIds === 'none' ? mdiCheck : undefined)">
<v-list-item class="text-sm" density="compact"
:key="TransactionTagFilter.TransactionNoTagFilterValue"
:value="TransactionTagFilter.TransactionNoTagFilterValue"
:class="{ 'list-item-selected': query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue }"
:append-icon="(query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue ? mdiCheck : undefined)">
<v-list-item-title class="cursor-pointer"
@click="changeTagFilter('none')">
@click="changeTagFilter(TransactionTagFilter.TransactionNoTagFilterValue)">
<div class="d-flex align-center">
<v-icon :icon="mdiBorderNoneVariant" />
<span class="text-sm ms-3">{{ tt('Without Tags') }}</span>
@@ -460,8 +462,8 @@
</v-list-item-title>
</v-list-item>
<v-list-item key="multiple" value="multiple" class="text-sm" density="compact"
:class="{ 'list-item-selected': query.tagIds && queryAllFilterTagIdsCount > 1 }"
:append-icon="(query.tagIds && queryAllFilterTagIdsCount > 1 ? mdiCheck : undefined)"
:class="{ 'list-item-selected': query.tagFilter && queryAllFilterTagIdsCount > 1 }"
:append-icon="(query.tagFilter && queryAllFilterTagIdsCount > 1 ? mdiCheck : undefined)"
v-if="allAvailableTagsCount > 0">
<v-list-item-title class="cursor-pointer"
@click="showFilterTagDialog = true">
@@ -472,34 +474,18 @@
</v-list-item-title>
</v-list-item>
<v-divider v-if="query.tagIds && query.tagIds !== 'none'" />
<template v-if="query.tagIds && query.tagIds !== 'none'">
<v-list-item class="text-sm" density="compact"
:key="filterType.type"
:value="filterType.type"
:append-icon="(query.tagFilterType === filterType.type ? mdiCheck : undefined)"
v-for="filterType in allTransactionTagFilterTypes">
<v-list-item-title class="cursor-pointer"
@click="changeTagFilterType(filterType.type)">
<div class="d-flex align-center">
<v-icon size="24" :icon="filterType.icon"/>
<span class="text-sm ms-3">{{ filterType.displayName }}</span>
</div>
</v-list-item-title>
</v-list-item>
</template>
<v-divider v-if="query.tagFilter && query.tagFilter !== TransactionTagFilter.TransactionNoTagFilterValue" />
<template :key="transactionTag.id"
v-for="transactionTag in allTransactionTags">
<v-divider v-if="!transactionTag.hidden || query.tagIds === transactionTag.id" />
<v-divider v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])" />
<v-list-item class="text-sm" density="compact"
:value="transactionTag.id"
:class="{ 'list-item-selected': query.tagIds === transactionTag.id, 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && queryAllFilterTagIds[transactionTag.id] }"
:append-icon="(query.tagIds === transactionTag.id ? mdiCheck : undefined)"
v-if="!transactionTag.hidden || query.tagIds === transactionTag.id">
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
:append-icon="(queryAllFilterTagIds[transactionTag.id] === true ? mdiCheck : (queryAllFilterTagIds[transactionTag.id] === false ? mdiClose : undefined))"
v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])">
<v-list-item-title class="cursor-pointer"
@click="changeTagFilter(transactionTag.id)">
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())">
<div class="d-flex align-center">
<v-icon size="24" :icon="mdiPound"/>
<span class="text-sm ms-3">{{ transactionTag.name }}</span>
@@ -678,7 +664,6 @@ import { useDesktopPageStore } from '@/stores/desktopPage.ts';
import {
type NameNumeralValue,
type TypeAndDisplayName,
keys
} from '@/core/base.ts';
import {
@@ -690,16 +675,18 @@ import {
} from '@/core/datetime.ts';
import { type NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
import { ThemeType } from '@/core/theme.ts';
import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts';
import { TransactionType } from '@/core/transaction.ts';
import { TemplateType } from '@/core/template.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import type { Transaction } from '@/models/transaction.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
import type { TransactionTemplate } from '@/models/transaction_template.ts';
import {
isDefined,
isObject,
isString,
isNumber
isNumber,
objectFieldWithValueToArrayItem
} from '@/lib/common.ts';
import {
getCurrentUnixTime,
@@ -731,6 +718,7 @@ import logger from '@/lib/logger.ts';
import {
mdiMagnify,
mdiCheck,
mdiClose,
mdiViewGridOutline,
mdiBorderNoneVariant,
mdiVectorArrangeBelow,
@@ -740,10 +728,6 @@ import {
mdiPencilBoxOutline,
mdiArrowLeft,
mdiArrowRight,
mdiPlusBoxMultipleOutline,
mdiCheckboxMultipleOutline,
mdiMinusBoxMultipleOutline,
mdiCloseBoxMultipleOutline,
mdiPound,
mdiMagicStaff,
mdiTextBoxOutline
@@ -757,8 +741,7 @@ interface TransactionListProps {
initType?: string,
initCategoryIds?: string,
initAccountIds?: string,
initTagIds?: string,
initTagFilterType?: string,
initTagFilter?: string,
initAmountFilter?: string,
initKeyword?: string
}
@@ -771,12 +754,6 @@ type EditDialogType = InstanceType<typeof EditDialog>;
type AIImageRecognitionDialogType = InstanceType<typeof AIImageRecognitionDialog>;
type ImportDialogType = InstanceType<typeof ImportDialog>;
interface TransactionTemplateWithIcon {
type: number;
displayName: string;
icon: string;
}
interface TransactionListDisplayTotalAmount {
income: string;
expense: string;
@@ -789,7 +766,6 @@ const theme = useTheme();
const {
tt,
getAllRecentMonthDateRanges,
getAllTransactionTagFilterTypes,
getWeekdayLongName,
getCurrentNumeralSystemType
} = useI18n();
@@ -852,13 +828,6 @@ const transactionsStore = useTransactionsStore();
const transactionTemplatesStore = useTransactionTemplatesStore();
const desktopPageStore = useDesktopPageStore();
const tagFilterIconMap: Record<number, string> = {
[TransactionTagFilterType.HasAny.type]: mdiPlusBoxMultipleOutline,
[TransactionTagFilterType.HasAll.type]: mdiCheckboxMultipleOutline,
[TransactionTagFilterType.NotHasAny.type]: mdiMinusBoxMultipleOutline,
[TransactionTagFilterType.NotHasAll.type]: mdiCloseBoxMultipleOutline
};
const timeFilterMenu = useTemplateRef<VMenu>('timeFilterMenu');
const categoryFilterMenu = useTemplateRef<VMenu>('categoryFilterMenu');
const amountFilterMenu = useTemplateRef<VMenu>('amountFilterMenu');
@@ -912,21 +881,6 @@ const allTransactionTemplates = computed<TransactionTemplate[]>(() => {
return allTemplates[TemplateType.Normal.type] || [];
});
const allTransactionTagFilterTypes = computed<TransactionTemplateWithIcon[]>(() => {
const allTagFilterTypes: TypeAndDisplayName[] = getAllTransactionTagFilterTypes();
const allTagFilterTypesWithIcon: TransactionTemplateWithIcon[] = [];
for (const tagFilterType of allTagFilterTypes) {
allTagFilterTypesWithIcon.push({
type: tagFilterType.type,
displayName: tagFilterType.displayName,
icon: tagFilterIconMap[tagFilterType.type] ?? ''
});
}
return allTagFilterTypesWithIcon;
});
const allowCategoryTypes = computed<string>(() => {
if (TransactionType.Income <= query.value.type && query.value.type <= TransactionType.Transfer) {
return transactionTypeToCategoryType(query.value.type)?.toString() ?? '';
@@ -1018,10 +972,16 @@ const queryAllSelectedFilterAccountIds = computed<string>(() => {
});
const queryAllSelectedFilterTagIds = computed<string>(() => {
if (queryAllFilterTagIdsCount.value === 0) {
if (query.value.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue) {
return TransactionTagFilter.TransactionNoTagFilterValue;
} else if (queryAllFilterTagIdsCount.value === 0) {
return '';
} else if (queryAllFilterTagIdsCount.value === 1) {
return query.value.tagIds;
for (const tagId of keys(queryAllFilterTagIds.value)) {
return tagId;
}
return '';
} else { // queryAllFilterTagIdsCount.value > 1
return 'multiple';
}
@@ -1147,8 +1107,7 @@ function init(initProps: TransactionListProps): void {
type: initProps.initType && parseInt(initProps.initType) > 0 ? parseInt(initProps.initType) : undefined,
categoryIds: initProps.initCategoryIds,
accountIds: initProps.initAccountIds,
tagIds: initProps.initTagIds,
tagFilterType: initProps.initTagFilterType && parseInt(initProps.initTagFilterType) >= 0 ? parseInt(initProps.initTagFilterType) : undefined,
tagFilter: initProps.initTagFilter,
amountFilter: initProps.initAmountFilter || '',
keyword: initProps.initKeyword || ''
});
@@ -1490,13 +1449,13 @@ function changeMultipleAccountsFilter(changed: boolean): void {
updateUrlWhenChanged(changed);
}
function changeTagFilter(tagIds: string): void {
if (query.value.tagIds === tagIds) {
function changeTagFilter(tagFilter: string): void {
if (query.value.tagFilter === tagFilter) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
tagIds: tagIds
tagFilter: tagFilter
});
updateUrlWhenChanged(changed);
@@ -1508,18 +1467,6 @@ function changeMultipleTagsFilter(changed: boolean): void {
updateUrlWhenChanged(changed);
}
function changeTagFilterType(filterType: number): void {
if (query.value.tagFilterType === filterType) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
tagFilterType: filterType
});
updateUrlWhenChanged(changed);
}
function changeKeywordFilter(keyword: string): void {
if (query.value.keyword === keyword) {
return;
@@ -1592,7 +1539,7 @@ function add(template?: TransactionTemplate): void {
type: query.value.type,
categoryId: queryAllFilterCategoryIdsCount.value === 1 ? query.value.categoryIds : '',
accountId: queryAllFilterAccountIdsCount.value === 1 ? query.value.accountIds : '',
tagIds: query.value.tagIds || '',
tagIds: objectFieldWithValueToArrayItem(queryAllFilterTagIds.value, true).join(',') || '',
template: template
}).then(result => {
if (result && result.message) {
@@ -1765,8 +1712,7 @@ onBeforeRouteUpdate((to) => {
initType: (to.query['type'] as string | null) || undefined,
initCategoryIds: (to.query['categoryIds'] as string | null) || undefined,
initAccountIds: (to.query['accountIds'] as string | null) || undefined,
initTagIds: (to.query['tagIds'] as string | null) || undefined,
initTagFilterType: (to.query['tagFilterType'] as string | null) || undefined,
initTagFilter: (to.query['tagFilter'] as string | null) || undefined,
initAmountFilter: (to.query['amountFilter'] as string | null) || undefined,
initKeyword: (to.query['keyword'] as string | null) || undefined
});

View File

@@ -57,7 +57,7 @@
v-for="typeName in parsedFileAllTransactionTypes">
<td>{{ typeName }}</td>
<td>
<v-btn-toggle class="transaction-types-toggle" density="compact" variant="outlined"
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
mandatory="force" divided
v-model="parsedFileDataColumnMapping.transactionTypeMapping[typeName]">
<v-btn :value="undefined">{{ tt('None') }}</v-btn>
@@ -166,14 +166,14 @@
v-for="separator in allSeparators">
<td>{{ separator.name }} ({{separator.value}})</td>
<td>
<v-btn-toggle class="transaction-types-toggle" density="compact" variant="outlined"
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
mandatory="force" divided
v-model="parsedFileDataColumnMapping.geoLocationOrder"
v-if="parsedFileDataColumnMapping.geoLocationSeparator === separator.value">
<v-btn value="latlon">{{ `${tt('Latitude')}${separator.value}${tt('Longitude')}` }}</v-btn>
<v-btn value="lonlat">{{ `${tt('Longitude')}${separator.value}${tt('Latitude')}` }}</v-btn>
</v-btn-toggle>
<v-btn-group class="transaction-types-toggle" density="compact" variant="outlined"
<v-btn-group class="toggle-buttons" density="compact" variant="outlined"
divided v-if="parsedFileDataColumnMapping.geoLocationSeparator !== separator.value">
<v-btn @click="parsedFileDataColumnMapping.setGeoLocationFormat(separator.value, 'latlon')">{{ `${tt('Latitude')}${separator.value}${tt('Longitude')}` }}</v-btn>
<v-btn @click="parsedFileDataColumnMapping.setGeoLocationFormat(separator.value, 'lonlat')">{{ `${tt('Longitude')}${separator.value}${tt('Latitude')}` }}</v-btn>
@@ -552,39 +552,3 @@ defineExpose({
saveColumnMappingFile
});
</script>
<style>
.transaction-types-popup-menu .transaction-types-toggle {
overflow-x: auto;
white-space: nowrap;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle {
height: auto !important;
padding: 0;
border: none;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn {
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle button.v-btn {
width: auto !important;
}
</style>

View File

@@ -40,14 +40,27 @@
</f7-list>
<f7-block class="combination-list-wrapper margin-vertical" key="default" v-show="!loading && hasAnyVisibleTag">
<f7-list class="margin-top-half margin-bottom" strong inset dividers v-if="query['type'] === 'statisticsCurrent'">
<f7-list class="margin-top-half margin-bottom" strong inset dividers>
<f7-list-item radio
:title="filterType.displayName"
:value="filterType.type"
:checked="tagFilterType === filterType.type"
:title="tt(filterType.name)"
:key="filterType.type"
v-for="filterType in allTagFilterTypes"
@change="tagFilterType = filterType.type">
:value="filterType.type"
:checked="includeTagFilterType === filterType.type"
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
@change="includeTagFilterType = filterType.type"
v-if="includeTagsCount > 1">
</f7-list-item>
</f7-list>
<f7-list class="margin-top-half margin-bottom" strong inset dividers>
<f7-list-item radio
:title="tt(filterType.name)"
:key="filterType.type"
:value="filterType.type"
:checked="excludeTagFilterType === filterType.type"
v-for="filterType in [TransactionTagFilterType.NotHasAny, TransactionTagFilterType.NotHasAll]"
@change="excludeTagFilterType = filterType.type"
v-if="excludeTagsCount > 1">
</f7-list-item>
</f7-list>
@@ -68,14 +81,15 @@
</f7-block-title>
<f7-accordion-content :style="{ height: collapseStates['default']!.opened ? 'auto' : '' }">
<f7-list strong inset dividers accordion-list class="combination-list-content">
<f7-list-item checkbox
<f7-list-item link="#"
popover-open=".tag-filter-state-popover-menu"
:title="transactionTag.name"
:value="transactionTag.id"
:checked="!filterTagIds[transactionTag.id]"
:key="transactionTag.id"
:after="tt(filterTagIds[transactionTag.id] === TransactionTagFilterState.Include ? 'Included' : filterTagIds[transactionTag.id] === TransactionTagFilterState.Exclude ? 'Excluded' : 'Default')"
v-for="transactionTag in allTags"
v-show="showHidden || !transactionTag.hidden"
@change="updateTransactionTagSelected">
@click="currentTransactionTagId = transactionTag.id">
<template #media>
<f7-icon class="transaction-tag-icon" f7="number">
<f7-badge color="gray" class="right-bottom-icon" v-if="transactionTag.hidden">
@@ -89,14 +103,35 @@
</f7-accordion-item>
</f7-block>
<f7-popover class="tag-filter-state-popover-menu"
v-model:opened="showTagFilterStatePopover">
<f7-list dividers>
<f7-list-item :title="state.displayName"
:class="{ 'list-item-selected': filterTagIds[currentTransactionTagId] === state.type }"
:key="state.type"
v-for="state in [
{ type: TransactionTagFilterState.Include, displayName: tt('Included') },
{ type: TransactionTagFilterState.Default, displayName: tt('Default') },
{ type: TransactionTagFilterState.Exclude, displayName: tt('Excluded') }
]"
@click="updateCurrentTransactionTagState(state.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="filterTagIds[currentTransactionTagId] === state.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectAllTransactionTags">{{ tt('Select All') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectNoneTransactionTags">{{ tt('Select None') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectInvertTransactionTags">{{ tt('Invert Selection') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(false, TransactionTagFilterState.Include)">{{ tt('Set All to Included') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(false, TransactionTagFilterState.Default)">{{ tt('Set All to Default') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(false, TransactionTagFilterState.Exclude)">{{ tt('Set All to Excluded') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectAllVisibleTransactionTags">{{ tt('Select All Visible') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(true, TransactionTagFilterState.Include)">{{ tt('Set All Visible Items to Included') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(true, TransactionTagFilterState.Default)">{{ tt('Set All Visible Items to Default') }}</f7-actions-button>
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(true, TransactionTagFilterState.Exclude)">{{ tt('Set All Visible Items to Excluded') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button v-if="!showHidden" @click="showHidden = true">{{ tt('Show Hidden Transaction Tags') }}</f7-actions-button>
@@ -115,16 +150,14 @@ import type { Router } from 'framework7/types';
import { useI18n } from '@/locales/helpers.ts';
import { useI18nUIComponents } from '@/lib/ui/mobile.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 {
selectAllVisible,
selectAll,
selectNone,
selectInvert
} from '@/lib/common.ts';
import { TransactionTagFilterType } from '@/core/transaction.ts';
interface CollapseState {
opened: boolean;
@@ -144,11 +177,13 @@ const {
loading,
showHidden,
filterTagIds,
tagFilterType,
includeTagFilterType,
excludeTagFilterType,
includeTagsCount,
excludeTagsCount,
title,
applyText,
allTags,
allTagFilterTypes,
hasAnyAvailableTag,
hasAnyVisibleTag,
loadFilterTagIds,
@@ -158,6 +193,8 @@ const {
const transactionTagsStore = useTransactionTagsStore();
const loadingError = ref<unknown | null>(null);
const currentTransactionTagId = ref<string>('');
const showTagFilterStatePopover = ref<boolean>(false);
const showMoreActionSheet = ref<boolean>(false);
const collapseStates = ref<Record<string, CollapseState>>({
@@ -186,32 +223,20 @@ function init(): void {
});
}
function updateTransactionTagSelected(e: Event): void {
const target = e.target as HTMLInputElement;
const transactionTagId = target.value;
const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId];
function updateCurrentTransactionTagState(state: number): void {
filterTagIds.value[currentTransactionTagId.value] = state;
showTagFilterStatePopover.value = false;
currentTransactionTagId.value = '';
}
if (!transactionTag) {
return;
function setAllToState(onlyVisible: boolean, value: TransactionTagFilterState): void {
for (const tag of allTags.value) {
if (onlyVisible && !showHidden.value && tag.hidden) {
continue;
}
filterTagIds.value[transactionTag.id] = !target.checked;
}
function selectAllTransactionTags(): void {
selectAll(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
}
function selectNoneTransactionTags(): void {
selectNone(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
}
function selectInvertTransactionTags(): void {
selectInvert(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
}
function selectAllVisibleTransactionTags(): void {
selectAllVisible(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
filterTagIds.value[tag.id] = value;
}
}
function save(): void {

View File

@@ -62,7 +62,7 @@
<span :class="{ 'tabbar-item-changed': query.accountIds }">{{ queryAccountName }}</span>
</f7-link>
<f7-link popover-open=".more-popover-menu">
<f7-icon f7="ellipsis_vertical" :class="{ 'tabbar-item-changed': query.type > 0 || query.amountFilter || query.tagIds }"></f7-icon>
<f7-icon f7="ellipsis_vertical" :class="{ 'tabbar-item-changed': query.type > 0 || query.amountFilter || query.tagFilter }"></f7-icon>
</f7-link>
</f7-toolbar>
@@ -509,52 +509,37 @@
<f7-list-item group-title>
<small>{{ tt('Tags') }}</small>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': !query.tagIds }" :title="tt('All')" @click="changeTagFilter('')">
<f7-list-item :class="{ 'list-item-selected': !query.tagFilter }" :title="tt('All')" @click="changeTagFilter('')">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.tagIds"></f7-icon>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.tagFilter"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.tagIds === 'none' }" :title="tt('Without Tags')" @click="changeTagFilter('none')">
<f7-list-item :class="{ 'list-item-selected': query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue }" :title="tt('Without Tags')" @click="changeTagFilter(TransactionTagFilter.TransactionNoTagFilterValue)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagIds === 'none'"></f7-icon>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :class="{ 'list-item-selected': query.tagIds && queryAllFilterTagIdsCount > 1 }"
<f7-list-item :class="{ 'list-item-selected': query.tagFilter && queryAllFilterTagIdsCount > 1 }"
:title="tt('Multiple Tags')" @click="filterMultipleTags()" v-if="allAvailableTagsCount > 0">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagIds && queryAllFilterTagIdsCount > 1"></f7-icon>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagFilter && queryAllFilterTagIdsCount > 1"></f7-icon>
</template>
</f7-list-item>
<template v-if="query.tagIds && query.tagIds !== 'none'">
<f7-list-item :title="filterType.displayName"
:key="filterType.type"
v-for="filterType in allTransactionTagFilterTypes"
@click="changeTagFilterType(filterType.type)"
>
<template #after>
<f7-icon class="list-item-checked-icon"
f7="checkmark_alt"
v-if="query.tagFilterType === filterType.type">
</f7-icon>
</template>
</f7-list-item>
</template>
<f7-list-item :title="transactionTag.name"
:class="{ 'list-item-selected': query.tagIds === transactionTag.id, 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && queryAllFilterTagIds[transactionTag.id] }"
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
:key="transactionTag.id"
v-for="transactionTag in allTransactionTags"
v-show="!transactionTag.hidden || query.tagIds === transactionTag.id"
@click="changeTagFilter(transactionTag.id)"
v-show="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])"
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())"
>
<template #before-title>
<f7-icon class="transaction-tag-name transaction-tag-icon" f7="number"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon"
f7="checkmark_alt"
v-if="query.tagIds === transactionTag.id">
:f7="queryAllFilterTagIds[transactionTag.id] === true ? 'checkmark_alt' : (queryAllFilterTagIds[transactionTag.id] === false ? 'multiply' : undefined)"
v-if="isDefined(queryAllFilterTagIds[transactionTag.id])">
</f7-icon>
</template>
</f7-list-item>
@@ -597,7 +582,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { type TransactionMonthList, useTransactionsStore } from '@/stores/transaction.ts';
import { type TypeAndDisplayName, keys } from '@/core/base.ts';
import { keys } from '@/core/base.ts';
import { TextDirection } from '@/core/text.ts';
import {
type TextualYearMonth,
@@ -609,10 +594,12 @@ import {
import { AmountFilterType } from '@/core/numeral.ts';
import { TransactionType } from '@/core/transaction.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import type { Transaction } from '@/models/transaction.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
import {
isNumber
isDefined,
isNumber,
objectFieldWithValueToArrayItem
} from '@/lib/common.ts';
import {
getCurrentUnixTime,
@@ -644,7 +631,6 @@ const props = defineProps<{
const {
tt,
getCurrentLanguageTextDirection,
getAllTransactionTagFilterTypes,
getWeekdayShortName,
getCalendarDisplayDayOfMonthFromUnixTime
} = useI18n();
@@ -723,8 +709,6 @@ const showDeleteActionSheet = ref<boolean>(false);
const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirection());
const isDarkMode = computed<boolean>(() => environmentsStore.framework7DarkMode || false);
const allTransactionTagFilterTypes = computed<TypeAndDisplayName[]>(() => getAllTransactionTagFilterTypes());
const transactions = computed<TransactionMonthList[]>(() => {
if (loading.value) {
return [];
@@ -925,8 +909,7 @@ function init(): void {
type: initQuery['type'] && parseInt(initQuery['type']) > 0 ? parseInt(initQuery['type']) : undefined,
categoryIds: initQuery['categoryIds'],
accountIds: initQuery['accountIds'],
tagIds: initQuery['tagIds'],
tagFilterType: initQuery['tagFilterType'] && parseInt(initQuery['tagFilterType']) >= 0 ? parseInt(initQuery['tagFilterType']) : undefined,
tagFilter: initQuery['tagFilter'],
keyword: initQuery['keyword']
});
@@ -1277,13 +1260,13 @@ function filterMultipleAccounts(): void {
props.f7router.navigate('/settings/filter/account?type=transactionListCurrent');
}
function changeTagFilter(tagIds: string): void {
if (query.value.tagIds === tagIds) {
function changeTagFilter(tagFilter: string): void {
if (query.value.tagFilter === tagFilter) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
tagIds: tagIds
tagFilter: tagFilter
});
showMorePopover.value = false;
@@ -1297,22 +1280,6 @@ function filterMultipleTags(): void {
props.f7router.navigate('/settings/filter/tag?type=transactionListCurrent');
}
function changeTagFilterType(filterType: number): void {
if (query.value.tagFilterType === filterType) {
return;
}
const changed = transactionsStore.updateTransactionListFilter({
tagFilterType: filterType
});
showMorePopover.value = false;
if (changed) {
reload();
}
}
function changeKeywordFilter(keyword: string): void {
if (query.value.keyword === keyword) {
return;
@@ -1383,8 +1350,8 @@ function add(): void {
params.push(`accountId=${query.value.accountIds}`);
}
if (query.value.tagIds) {
params.push(`tagIds=${query.value.tagIds}`);
if (query.value.tagFilter) {
params.push(`tagIds=${objectFieldWithValueToArrayItem(queryAllFilterTagIds.value, true).join(',') || ''}`);
}
props.f7router.navigate(`/transaction/add?${params.join('&')}`);