diff --git a/cmd/webserver.go b/cmd/webserver.go index 073b4196..3e9090dd 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -89,6 +89,7 @@ func startWebServer(c *cli.Context) error { _ = v.RegisterValidation("validEmail", validators.ValidEmail) _ = v.RegisterValidation("validCurrency", validators.ValidCurrency) _ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor) + _ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter) } router.NoRoute(bindApi(api.Default.ApiNotFound)) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 4d68ca53..e7099ee5 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -59,7 +59,7 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (any, *errs.E return nil, errs.Or(err, errs.ErrOperationFailed) } - totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, transactionCountReq.Keyword) + totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, transactionCountReq.AmountFilter, transactionCountReq.Keyword) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) @@ -118,7 +118,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Er var totalCount int64 if transactionListReq.WithCount { - totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword) + totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.AmountFilter, transactionListReq.Keyword) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) @@ -126,7 +126,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Er } } - transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true) + transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error()) @@ -206,7 +206,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (any, *er return nil, errs.Or(err, errs.ErrOperationFailed) } - transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword) + transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.AmountFilter, transactionListReq.Keyword) if err != nil { log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error()) diff --git a/pkg/errs/global.go b/pkg/errs/global.go index 00ec3116..9861d135 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -82,3 +82,8 @@ func GetParameterInvalidCurrencyMessage(field string) string { func GetParameterInvalidHexRGBColorMessage(field string) string { return fmt.Sprintf("parameter \"%s\" is invalid color", field) } + +// GetParameterInvalidAmountFilterMessage returns specific error message for invalid amount filter parameter error +func GetParameterInvalidAmountFilterMessage(field string) string { + return fmt.Sprintf("parameter \"%s\" is invalid amount filter", field) +} diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index a095e0db..9bb5786e 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -94,12 +94,13 @@ type TransactionModifyRequest struct { // TransactionCountRequest represents transaction count request type TransactionCountRequest struct { - Type TransactionDbType `form:"type" binding:"min=0,max=4"` - CategoryId int64 `form:"category_id" binding:"min=0"` - AccountId int64 `form:"account_id" binding:"min=0"` - Keyword string `form:"keyword"` - MaxTime int64 `form:"max_time" binding:"min=0"` - MinTime int64 `form:"min_time" binding:"min=0"` + Type TransactionDbType `form:"type" binding:"min=0,max=4"` + CategoryId int64 `form:"category_id" binding:"min=0"` + AccountId int64 `form:"account_id" binding:"min=0"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` + Keyword string `form:"keyword"` + MaxTime int64 `form:"max_time" binding:"min=0"` + MinTime int64 `form:"min_time" binding:"min=0"` } // TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request @@ -107,6 +108,7 @@ type TransactionListByMaxTimeRequest struct { Type TransactionDbType `form:"type" binding:"min=0,max=4"` CategoryId int64 `form:"category_id" binding:"min=0"` AccountId int64 `form:"account_id" binding:"min=0"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` Keyword string `form:"keyword"` MaxTime int64 `form:"max_time" binding:"min=0"` MinTime int64 `form:"min_time" binding:"min=0"` @@ -125,6 +127,7 @@ type TransactionListInMonthByPageRequest struct { Type TransactionDbType `form:"type" binding:"min=0,max=4"` CategoryId int64 `form:"category_id" binding:"min=0"` AccountId int64 `form:"account_id" binding:"min=0"` + AmountFilter string `form:"amount_filter" binding:"validAmountFilter"` Keyword string `form:"keyword"` TrimAccount bool `form:"trim_account"` TrimCategory bool `form:"trim_category"` diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 60ea54b7..3aa568aa 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -73,11 +73,11 @@ func (s *TransactionService) GetAllTransactions(c *core.Context, uid int64, page // 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, "", 1, count, false, noDuplicated) + return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, "", "", 1, count, false, noDuplicated) } // GetTransactionsByMaxTime returns transactions before given time -func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { +func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -101,14 +101,14 @@ func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64 actualCount++ } - condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, noDuplicated) + condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, amountFilter, keyword, noDuplicated) err = s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions) return transactions, err } // GetTransactionsInMonthByPage returns all transactions in given year and month -func (s *TransactionService) GetTransactionsInMonthByPage(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string) ([]*models.Transaction, error) { +func (s *TransactionService) GetTransactionsInMonthByPage(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, amountFilter string, keyword string) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -121,7 +121,7 @@ func (s *TransactionService) GetTransactionsInMonthByPage(c *core.Context, uid i var transactions []*models.Transaction - condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true) + condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, amountFilter, keyword, true) err = s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("transaction_time desc").Find(&transactions) transactionsInMonth := make([]*models.Transaction, 0, len(transactions)) @@ -163,11 +163,11 @@ func (s *TransactionService) GetTransactionByTransactionId(c *core.Context, uid // 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, "") + return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, "", "") } // GetMonthTransactionCount returns total count of transactions in given year and month -func (s *TransactionService) GetMonthTransactionCount(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, utcOffset int16) (int64, error) { +func (s *TransactionService) GetMonthTransactionCount(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, amountFilter string, keyword string, utcOffset int16) (int64, error) { if uid <= 0 { return 0, errs.ErrUserIdInvalid } @@ -183,16 +183,16 @@ func (s *TransactionService) GetMonthTransactionCount(c *core.Context, uid int64 minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startTime.Unix()) maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endTime.Unix()) - 1 - return s.GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword) + return s.GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, amountFilter, keyword) } // GetTransactionCount returns count of transactions -func (s *TransactionService) GetTransactionCount(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string) (int64, error) { +func (s *TransactionService) GetTransactionCount(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, amountFilter string, keyword string) (int64, error) { if uid <= 0 { return 0, errs.ErrUserIdInvalid } - condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true) + condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, amountFilter, keyword, true) return s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).Count(&models.Transaction{}) } @@ -1331,7 +1331,7 @@ func (s *TransactionService) GetTransactionMapByList(transactions []*models.Tran return transactionMap } -func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, noDuplicated bool) (string, []any) { +func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) { condition := "uid=? AND deleted=?" conditionParams := make([]any, 0, 16) conditionParams = append(conditionParams, uid) @@ -1399,6 +1399,58 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact condition = condition + " AND account_id IN (" + conditions.String() + ")" } + if amountFilter != "" { + amountFilterItems := strings.Split(amountFilter, ":") + + if len(amountFilterItems) == 2 && amountFilterItems[0] == "gt" { + value, err := utils.StringToInt64(amountFilterItems[1]) + + if err == nil { + condition = condition + " AND amount > ?" + conditionParams = append(conditionParams, value) + } + } else if len(amountFilterItems) == 2 && amountFilterItems[0] == "lt" { + value, err := utils.StringToInt64(amountFilterItems[1]) + + if err == nil { + condition = condition + " AND amount < ?" + conditionParams = append(conditionParams, value) + } + } else if len(amountFilterItems) == 2 && amountFilterItems[0] == "eq" { + value, err := utils.StringToInt64(amountFilterItems[1]) + + if err == nil { + condition = condition + " AND amount = ?" + conditionParams = append(conditionParams, value) + } + } else if len(amountFilterItems) == 2 && amountFilterItems[0] == "ne" { + value, err := utils.StringToInt64(amountFilterItems[1]) + + if err == nil { + condition = condition + " AND amount <> ?" + conditionParams = append(conditionParams, value) + } + } else if len(amountFilterItems) == 3 && amountFilterItems[0] == "bt" { + value1, err := utils.StringToInt64(amountFilterItems[1]) + value2, err := utils.StringToInt64(amountFilterItems[2]) + + if err == nil { + condition = condition + " AND amount >= ? AND amount <= ?" + conditionParams = append(conditionParams, value1) + conditionParams = append(conditionParams, value2) + } + } else if len(amountFilterItems) == 3 && amountFilterItems[0] == "nb" { + value1, err := utils.StringToInt64(amountFilterItems[1]) + value2, err := utils.StringToInt64(amountFilterItems[2]) + + if err == nil { + condition = condition + " AND (amount < ? OR amount > ?)" + conditionParams = append(conditionParams, value1) + conditionParams = append(conditionParams, value2) + } + } + } + if keyword != "" { condition = condition + " AND comment LIKE ?" conditionParams = append(conditionParams, "%%"+keyword+"%%") diff --git a/pkg/utils/api.go b/pkg/utils/api.go index 9492357b..6a63631b 100644 --- a/pkg/utils/api.go +++ b/pkg/utils/api.go @@ -110,6 +110,8 @@ func getValidationErrorText(err validator.FieldError) string { return errs.GetParameterInvalidCurrencyMessage(fieldName) case "validHexRGBColor": return errs.GetParameterInvalidHexRGBColorMessage(fieldName) + case "validAmountFilter": + return errs.GetParameterInvalidAmountFilterMessage(fieldName) } return errs.GetParameterInvalidMessage(fieldName) diff --git a/pkg/validators/amount_filter.go b/pkg/validators/amount_filter.go new file mode 100644 index 00000000..598a9725 --- /dev/null +++ b/pkg/validators/amount_filter.go @@ -0,0 +1,54 @@ +package validators + +import ( + "strings" + + "github.com/go-playground/validator/v10" + + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +// ValidAmountFilter returns whether the given amount filter is valid +func ValidAmountFilter(fl validator.FieldLevel) bool { + if value, ok := fl.Field().Interface().(string); ok { + if value == "" { + return true + } + + amountFilterItems := strings.Split(value, ":") + + if len(amountFilterItems) < 2 { + return false + } + + amount1, err := utils.StringToInt64(amountFilterItems[1]) + + if err != nil { + return false + } + + if amountFilterItems[0] == "gt" || amountFilterItems[0] == "lt" || amountFilterItems[0] == "eq" || amountFilterItems[0] == "ne" { + if len(amountFilterItems) != 2 { + return false + } + } else if amountFilterItems[0] == "bt" || amountFilterItems[0] == "nb" { + if len(amountFilterItems) != 3 { + return false + } + + amount2, err := utils.StringToInt64(amountFilterItems[2]) + + if err != nil { + return false + } + + if amount2 < amount1 { + return false + } + } + + return true + } + + return false +} diff --git a/src/consts/numeral.js b/src/consts/numeral.js index db010ec4..ca136caf 100644 --- a/src/consts/numeral.js +++ b/src/consts/numeral.js @@ -86,6 +86,57 @@ const allDigitGroupingTypeMap = { [allDigitGroupingType.ThousandsSeparator.type]: allDigitGroupingType.ThousandsSeparator }; +const allAmountFilterType = { + GreaterThan: { + type: 'gt', + name: 'Greater than', + paramCount: 1 + }, + LessThan:{ + type: 'lt', + name: 'Less than', + paramCount: 1 + }, + EqualTo:{ + type: 'eq', + name: 'Equal to', + paramCount: 1 + }, + NotEqualTo:{ + type: 'ne', + name: 'Not equal to', + paramCount: 1 + }, + Between:{ + type: 'bt', + name: 'Between', + paramCount: 2 + }, + NotBetween:{ + type: 'nb', + name: 'Not between', + paramCount: 2 + } +}; + +const allAmountFilterTypeArray = [ + allAmountFilterType.GreaterThan, + allAmountFilterType.LessThan, + allAmountFilterType.EqualTo, + allAmountFilterType.NotEqualTo, + allAmountFilterType.Between, + allAmountFilterType.NotBetween, +]; + +const allAmountFilterTypeMap = { + [allAmountFilterType.GreaterThan.type]: allAmountFilterType.GreaterThan, + [allAmountFilterType.LessThan.type]: allAmountFilterType.LessThan, + [allAmountFilterType.EqualTo.type]: allAmountFilterType.EqualTo, + [allAmountFilterType.NotEqualTo.type]: allAmountFilterType.NotEqualTo, + [allAmountFilterType.Between.type]: allAmountFilterType.Between, + [allAmountFilterType.NotBetween.type]: allAmountFilterType.NotBetween +}; + const defaultDecimalSeparator = allDecimalSeparator.Dot; const defaultDigitGroupingSymbol = allDigitGroupingSymbol.Comma; const defaultDigitGroupingType = allDigitGroupingType.ThousandsSeparator; @@ -101,6 +152,9 @@ export default { allDigitGroupingType: allDigitGroupingType, allDigitGroupingTypeArray: allDigitGroupingTypeArray, allDigitGroupingTypeMap: allDigitGroupingTypeMap, + allAmountFilterType: allAmountFilterType, + allAmountFilterTypeArray: allAmountFilterTypeArray, + allAmountFilterTypeMap: allAmountFilterTypeMap, defaultDecimalSeparator: defaultDecimalSeparator, defaultDigitGroupingSymbol: defaultDigitGroupingSymbol, defaultDigitGroupingType: defaultDigitGroupingType, diff --git a/src/lib/i18n.js b/src/lib/i18n.js index 0fdf3595..cd79c2ea 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -175,6 +175,14 @@ const parameterizedErrors = [ field: 'parameter', localized: true }] + }, + { + localeKey: 'parameter invalid amount filter', + regex: /^parameter "(\w+)" is invalid amount filter$/, + parameters: [{ + field: 'parameter', + localized: true + }] } ]; diff --git a/src/lib/services.js b/src/lib/services.js index 2bab78a5..6fb321ff 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -279,13 +279,15 @@ export default { id }); }, - getTransactions: ({ maxTime, minTime, count, page, withCount, type, categoryId, accountId, keyword }) => { + getTransactions: ({ maxTime, minTime, count, page, withCount, type, categoryId, accountId, amountFilter, keyword }) => { + amountFilter = encodeURIComponent(amountFilter); keyword = encodeURIComponent(keyword); - return axios.get(`v1/transactions/list.json?max_time=${maxTime}&min_time=${minTime}&type=${type}&category_id=${categoryId}&account_id=${accountId}&keyword=${keyword}&count=${count}&page=${page}&with_count=${withCount}&trim_account=true&trim_category=true&trim_tag=true`); + return axios.get(`v1/transactions/list.json?max_time=${maxTime}&min_time=${minTime}&type=${type}&category_id=${categoryId}&account_id=${accountId}&amount_filter=${amountFilter}&keyword=${keyword}&count=${count}&page=${page}&with_count=${withCount}&trim_account=true&trim_category=true&trim_tag=true`); }, - getAllTransactionsByMonth: ({ year, month, type, categoryId, accountId, keyword }) => { + getAllTransactionsByMonth: ({ year, month, type, categoryId, accountId, amountFilter, keyword }) => { + amountFilter = encodeURIComponent(amountFilter); keyword = encodeURIComponent(keyword); - return axios.get(`v1/transactions/list/by_month.json?year=${year}&month=${month}&type=${type}&category_id=${categoryId}&account_id=${accountId}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); + return axios.get(`v1/transactions/list/by_month.json?year=${year}&month=${month}&type=${type}&category_id=${categoryId}&account_id=${accountId}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); }, getTransactionStatistics: ({ startTime, endTime, useTransactionTimezone }) => { const queryParams = []; diff --git a/src/locales/en.js b/src/locales/en.js index 536c412e..f36cc246 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -707,6 +707,7 @@ export default { 'time': 'Time', 'startTime': 'Start Time', 'endTime': 'End Time', + 'amountFilter': 'Amount Filter', 'sourceAccountId': 'Source Account ID', 'destinationAccountId': 'Destination Account ID', 'sourceAmount': 'Source Amount', @@ -731,6 +732,7 @@ export default { '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', }, 'OK': 'OK', 'Cancel': 'Cancel', @@ -799,6 +801,12 @@ export default { 'Select Time': 'Select Time', 'Now': 'Now', 'Custom': 'Custom', + 'Greater than': 'Greater than', + 'Less than': 'Less than', + 'Equal to': 'Equal to', + 'Not equal to': 'Not equal to', + 'Between': 'Between', + 'Not between': 'Not between', 'Pie Chart': 'Pie Chart', 'Bar Chart': 'Bar Chart', 'Area Chart': 'Area Chart', @@ -921,7 +929,9 @@ export default { 'This Month': 'This Month', 'This Year': 'This Year', 'Monthly income': 'Monthly income', + 'Filter Amount': 'Filter Amount', 'Unable to retrieve transaction overview': 'Unable to retrieve transaction overview', + 'Incorrect amount range': 'Incorrect amount range', 'Data is up to date': 'Data is up to date', 'Data has been updated': 'Data has been updated', 'Net assets': 'Net assets', @@ -1066,6 +1076,8 @@ export default { 'Default Sort Order': 'Default Sort Order', 'Timezone Used for Date Range': 'Timezone Used for Date Range', 'Amount': 'Amount', + 'Min Amount': 'Min Amount', + 'Max Amount': 'Max Amount', 'Display Order': 'Display Order', 'Name': 'Name', 'Sort by Amount': 'Sort by Amount', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 8f555aa2..8720197a 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -707,6 +707,7 @@ export default { 'time': '时间', 'startTime': '开始时间', 'endTime': '结束时间', + 'amountFilter': '金额过滤', 'sourceAccountId': '来源账户ID', 'destinationAccountId': '目标账户ID', 'sourceAmount': '源金额', @@ -731,6 +732,7 @@ export default { 'parameter invalid email format': '{parameter}格式错误', 'parameter invalid currency': '{parameter}格式错误', 'parameter invalid color': '{parameter}格式错误', + 'parameter invalid amount filter': '{parameter}格式错误', }, 'OK': '确定', 'Cancel': '取消', @@ -799,6 +801,12 @@ export default { 'Select Time': '选择时间', 'Now': '现在', 'Custom': '自定义', + 'Greater than': '大于', + 'Less than': '小于', + 'Equal to': '等于', + 'Not equal to': '不等于', + 'Between': '介于', + 'Not between': '不介于', 'Pie Chart': '饼图', 'Bar Chart': '条形图', 'Area Chart': '面积图', @@ -921,7 +929,9 @@ export default { 'This Month': '本月', 'This Year': '今年', 'Monthly income': '当月收入', + 'Filter Amount': '过滤金额', 'Unable to retrieve transaction overview': '无法获取交易概要', + 'Incorrect amount range': '金额范围错误', 'Data is up to date': '数据已是最新', 'Data has been updated': '数据已更新', 'Net assets': '净资产', @@ -1066,6 +1076,8 @@ export default { 'Default Sort Order': '默认排序方式', 'Timezone Used for Date Range': '时间范围使用的时区', 'Amount': '金额', + 'Min Amount': '最小金额', + 'Max Amount': '最大金额', 'Display Order': '显示顺序', 'Name': '名称', 'Sort by Amount': '按金额排序', diff --git a/src/router/desktop.js b/src/router/desktop.js index b4ab7008..65d183ac 100644 --- a/src/router/desktop.js +++ b/src/router/desktop.js @@ -100,6 +100,7 @@ const router = createRouter({ initType: route.query.type, initCategoryId: route.query.categoryId, initAccountId: route.query.accountId, + initAmountFilter: route.query.amountFilter, initKeyword: route.query.keyword }) }, diff --git a/src/router/mobile.js b/src/router/mobile.js index 8b1e3999..411d3259 100644 --- a/src/router/mobile.js +++ b/src/router/mobile.js @@ -7,6 +7,7 @@ import UnlockPage from '@/views/mobile/UnlockPage.vue'; import TransactionListPage from '@/views/mobile/transactions/ListPage.vue'; import TransactionEditPage from '@/views/mobile/transactions/EditPage.vue'; +import TransactionAmountFilterPage from '@/views/mobile/transactions/AmountFilterPage.vue'; import AccountListPage from '@/views/mobile/accounts/ListPage.vue'; import AccountEditPage from '@/views/mobile/accounts/EditPage.vue'; @@ -148,6 +149,11 @@ const routes = [ async: asyncResolve(TransactionListPage), beforeEnter: [checkLogin] }, + { + path: '/transaction/filter/amount', + async: asyncResolve(TransactionAmountFilterPage), + beforeEnter: [checkLogin] + }, { path: '/transaction/add', async: asyncResolve(TransactionEditPage), diff --git a/src/stores/transaction.js b/src/stores/transaction.js index b93a1d56..c5ed27bb 100644 --- a/src/stores/transaction.js +++ b/src/stores/transaction.js @@ -270,6 +270,7 @@ export const useTransactionsStore = defineStore('transactions', { type: 0, categoryId: '0', accountId: '0', + amountFilter: '', keyword: '' }, transactions: [], @@ -365,6 +366,7 @@ export const useTransactionsStore = defineStore('transactions', { this.transactionsFilter.type = 0; this.transactionsFilter.categoryId = '0'; this.transactionsFilter.accountId = '0'; + this.transactionsFilter.amountFilter = ''; this.transactionsFilter.keyword = ''; this.transactions = []; this.transactionsNextTimeId = 0; @@ -412,6 +414,12 @@ export const useTransactionsStore = defineStore('transactions', { this.transactionsFilter.accountId = '0'; } + if (filter && isString(filter.amountFilter)) { + this.transactionsFilter.amountFilter = filter.amountFilter; + } else { + this.transactionsFilter.amountFilter = ''; + } + if (filter && isString(filter.keyword)) { this.transactionsFilter.keyword = filter.keyword; } else { @@ -443,6 +451,10 @@ export const useTransactionsStore = defineStore('transactions', { this.transactionsFilter.accountId = filter.accountId; } + if (filter && isString(filter.amountFilter)) { + this.transactionsFilter.amountFilter = filter.amountFilter; + } + if (filter && isString(filter.keyword)) { this.transactionsFilter.keyword = filter.keyword; } @@ -469,6 +481,10 @@ export const useTransactionsStore = defineStore('transactions', { querys.push('minTime=' + this.transactionsFilter.minTime); } + if (this.transactionsFilter.amountFilter) { + querys.push('amountFilter=' + encodeURIComponent(this.transactionsFilter.amountFilter)); + } + if (this.transactionsFilter.keyword) { querys.push('keyword=' + encodeURIComponent(this.transactionsFilter.keyword)); } @@ -497,6 +513,7 @@ export const useTransactionsStore = defineStore('transactions', { type: self.transactionsFilter.type, categoryId: self.transactionsFilter.categoryId, accountId: self.transactionsFilter.accountId, + amountFilter: self.transactionsFilter.amountFilter, keyword: self.transactionsFilter.keyword }).then(response => { const data = response.data; @@ -571,6 +588,7 @@ export const useTransactionsStore = defineStore('transactions', { type: self.transactionsFilter.type, categoryId: self.transactionsFilter.categoryId, accountId: self.transactionsFilter.accountId, + amountFilter: self.transactionsFilter.amountFilter, keyword: self.transactionsFilter.keyword }).then(response => { const data = response.data; diff --git a/src/views/desktop/transactions/ListPage.vue b/src/views/desktop/transactions/ListPage.vue index eabfb4cc..71e63815 100644 --- a/src/views/desktop/transactions/ListPage.vue +++ b/src/views/desktop/transactions/ListPage.vue @@ -192,7 +192,56 @@ - {{ $t('Amount') }} + + + + + + +
+ {{ $t('All') }} +
+
+
+ +
+
+ 0 ? parseInt(query.type) : undefined, categoryId: query.categoryId, accountId: query.accountId, + amountFilter: query.amountFilter || '', keyword: query.keyword || '' }); this.searchKeyword = query.keyword || ''; + this.currentAmountFilterType = ''; this.currentPage = 1; this.reload(false); @@ -855,6 +937,50 @@ export default { this.transactionsStore.clearTransactions(); this.$router.push(this.getFilterLinkUrl()); }, + changeAmountFilter(filterType) { + this.currentAmountFilterType = ''; + this.amountMenuState = false; + + if (this.query.amountFilter === filterType) { + return; + } + + let amountFilter = filterType; + + if (filterType) { + const amountCount = this.getAmountFilterParameterCount(filterType); + + if (!amountCount) { + return; + } + + if (amountCount === 1) { + amountFilter += ':' + this.currentAmountFilterValue1; + } else if (amountCount === 2) { + if (this.currentAmountFilterValue2 < this.currentAmountFilterValue1) { + this.$refs.snackbar.showMessage('Incorrect amount range'); + return; + } + + amountFilter += ':' + this.currentAmountFilterValue1 + ':' + this.currentAmountFilterValue2; + } else { + return; + } + } + + if (this.query.amountFilter === amountFilter) { + return; + } + + this.transactionsStore.updateTransactionListFilter({ + amountFilter: amountFilter + }); + + this.loading = true; + this.currentPageTransactions = []; + this.transactionsStore.clearTransactions(); + this.$router.push(this.getFilterLinkUrl()); + }, changeAccountFilter(accountId) { if (this.query.accountId === accountId) { return; @@ -929,6 +1055,34 @@ export default { this.scrollMenuToSelectedItem(this.$refs.categoryFilterMenu); } }, + scrollAmountMenuToSelectedItem(opened) { + if (opened) { + this.currentAmountFilterType = ''; + + let amount1 = 0, amount2 = 0; + + if (isString(this.query.amountFilter)) { + try { + const filterItems = this.query.amountFilter.split(':'); + const amountCount = this.getAmountFilterParameterCount(filterItems[0]); + + if (filterItems.length === 2 && amountCount === 1) { + amount1 = parseInt(filterItems[1]); + } else if (filterItems.length === 3 && amountCount === 2) { + amount1 = parseInt(filterItems[1]); + amount2 = parseInt(filterItems[2]); + } + } catch (ex) { + logger.warn('cannot parse amount from filter value, original value is ' + this.query.amountFilter); + } + } + + this.currentAmountFilterValue1 = amount1; + this.currentAmountFilterValue2 = amount2; + + this.scrollMenuToSelectedItem(this.$refs.amountFilterMenu); + } + }, scrollAccountMenuToSelectedItem(opened) { if (opened) { this.scrollMenuToSelectedItem(this.$refs.accountFilterMenu); @@ -1005,6 +1159,10 @@ export default { return []; }, + getAmountFilterParameterCount(filterType) { + const amountFilterType = numeralConstants.allAmountFilterTypeMap[filterType]; + return amountFilterType ? amountFilterType.paramCount : 0; + }, getFilterLinkUrl() { return `/transaction/list?${this.transactionsStore.getTransactionListPageParams()}`; } @@ -1071,11 +1229,21 @@ export default { } .transaction-category-menu .item-icon, +.transaction-amount-menu .item-icon, .transaction-account-menu .item-icon, .transaction-table .item-icon { padding-bottom: 3px; } +.transaction-amount-filter-value { + width: 100px; +} + +.transaction-amount-filter-value input.v-field__input { + min-height: 32px !important; + padding: 0 8px 0 8px; +} + .transaction-category-menu .has-children-item-selected span { font-weight: bold; } diff --git a/src/views/mobile/transactions/AmountFilterPage.vue b/src/views/mobile/transactions/AmountFilterPage.vue new file mode 100644 index 00000000..c07c8c50 --- /dev/null +++ b/src/views/mobile/transactions/AmountFilterPage.vue @@ -0,0 +1,180 @@ + + + diff --git a/src/views/mobile/transactions/ListPage.vue b/src/views/mobile/transactions/ListPage.vue index 6060cdc3..0c85c9fc 100644 --- a/src/views/mobile/transactions/ListPage.vue +++ b/src/views/mobile/transactions/ListPage.vue @@ -42,7 +42,7 @@ {{ queryAccountName }} - + @@ -403,6 +403,23 @@ + + + + + + + + @@ -426,6 +443,7 @@ import { useAccountsStore } from '@/stores/account.js'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js'; import { useTransactionsStore } from '@/stores/transaction.js'; +import numeralConstants from '@/consts/numeral.js'; import datetimeConstants from '@/consts/datetime.js'; import currencyConstants from '@/consts/currency.js'; import accountConstants from '@/consts/account.js'; @@ -518,6 +536,25 @@ export default { queryAccountName() { return getNameByKeyValue(this.allAccounts, this.query.accountId, null, 'name', this.$t('Account')); }, + queryAmount() { + if (!this.query.amountFilter) { + return ''; + } + + const amountFilterItems = this.query.amountFilter.split(':'); + + if (amountFilterItems.length < 2) { + return ''; + } + + const displayAmount = []; + + for (let i = 1; i < amountFilterItems.length; i++) { + displayAmount.push(this.getDisplayCurrency(amountFilterItems[i], false)); + } + + return displayAmount.join(' ~ '); + }, transactions() { if (this.loading) { return []; @@ -531,6 +568,9 @@ export default { hasMoreTransaction() { return this.transactionsStore.hasMoreTransaction; }, + allAmountFilterTypes() { + return numeralConstants.allAmountFilterTypeArray; + }, allTransactionTypes() { return transactionConstants.allTransactionTypes; }, @@ -777,6 +817,24 @@ export default { this.showAccountPopover = false; this.reload(null); }, + changeAmountFilter(filterType) { + if (this.query.amountFilter === filterType) { + return; + } + + if (filterType) { + this.showMorePopover = false; + this.f7router.navigate(`/transaction/filter/amount?type=${filterType}&value=${this.query.amountFilter}`); + return; + } + + this.transactionsStore.updateTransactionListFilter({ + amountFilter: filterType + }); + + this.showMorePopover = false; + this.reload(null); + }, changeKeywordFilter(keyword) { if (this.query.keyword === keyword) { return; @@ -1008,7 +1066,10 @@ export default { text-overflow: ellipsis; } -.date-popover-menu .popover-inner, .category-popover-menu .popover-inner, .account-popover-menu .popover-inner { +.date-popover-menu .popover-inner, +.category-popover-menu .popover-inner, +.account-popover-menu .popover-inner, +.more-popover-menu .popover-inner{ max-height: 400px; overflow-y: auto; }