support filtering transaction amount

This commit is contained in:
MaysWind
2024-06-30 21:42:21 +08:00
parent d2b3900ed4
commit b5d72c89f2
18 changed files with 668 additions and 29 deletions
+1
View File
@@ -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))
+4 -4
View File
@@ -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())
+5
View File
@@ -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)
}
+9 -6
View File
@@ -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"`
+63 -11
View File
@@ -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+"%%")
+2
View File
@@ -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)
+54
View File
@@ -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
}
+54
View File
@@ -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,
+8
View File
@@ -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
}]
}
];
+6 -4
View File
@@ -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 = [];
+12
View File
@@ -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',
+12
View File
@@ -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': '按金额排序',
+1
View File
@@ -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
})
},
+6
View File
@@ -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),
+18
View File
@@ -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;
+170 -2
View File
@@ -192,7 +192,56 @@
</v-list>
</v-menu>
</th>
<th class="transaction-table-column-amount">{{ $t('Amount') }}</th>
<th class="transaction-table-column-amount">
<v-menu ref="amountFilterMenu" class="transaction-amount-menu"
eager location="bottom" max-height="500"
:close-on-content-click="false"
v-model="amountMenuState"
@update:model-value="scrollAmountMenuToSelectedItem">
<template #activator="{ props }">
<div class="d-flex align-center cursor-pointer"
:class="{ 'readonly': loading, 'text-primary': query.amountFilter }" v-bind="props">
<span>{{ $t('Amount') }}</span>
<v-icon :icon="icons.dropdownMenu" />
</div>
</template>
<v-list :selected="[query.amountFilter.split(':')[0]]">
<v-list-item key="0" value="0" class="text-sm" density="compact"
:class="{ 'list-item-selected': !query.amountFilter }"
:append-icon="(!query.amountFilter && !currentAmountFilterType ? icons.check : null)">
<v-list-item-title class="cursor-pointer"
@click="changeAmountFilter('')">
<div class="d-flex align-center">
<span class="text-sm ml-3">{{ $t('All') }}</span>
</div>
</v-list-item-title>
</v-list-item>
<template :key="filterType.type"
v-for="filterType in allAmountFilterTypes">
<v-list-item class="text-sm" density="compact"
:value="filterType.type"
:class="{ 'list-item-selected': query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`) }"
:append-icon="(query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`) && currentAmountFilterType !== filterType.type ? icons.check : null)">
<v-list-item-title class="cursor-pointer"
@click="currentAmountFilterType = filterType.type">
<div class="d-flex align-center">
<span class="text-sm ml-3">{{ $t(filterType.name) }}</span>
<span class="text-sm ml-4" v-if="query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`) && currentAmountFilterType !== filterType.type">{{ queryAmount }}</span>
<amount-input class="transaction-amount-filter-value ml-4" density="compact" v-model="currentAmountFilterValue1"
v-if="currentAmountFilterType === filterType.type"/>
<span class="ml-2 mr-2" v-if="currentAmountFilterType === filterType.type && filterType.paramCount === 2">~</span>
<amount-input class="transaction-amount-filter-value" density="compact" v-model="currentAmountFilterValue2"
v-if="currentAmountFilterType === filterType.type && filterType.paramCount === 2"/>
<v-btn class="ml-2" density="compact" color="primary" variant="tonal"
@click="changeAmountFilter(filterType.type)"
v-if="currentAmountFilterType === filterType.type">{{ $t('Apply') }}</v-btn>
</div>
</v-list-item-title>
</v-list-item>
</template>
</v-list>
</v-menu>
</th>
<th class="transaction-table-column-account">
<v-menu ref="accountFilterMenu" class="transaction-account-menu"
eager location="bottom" max-height="500"
@@ -353,11 +402,13 @@ 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';
import transactionConstants from '@/consts/transaction.js';
import { getNameByKeyValue } from '@/lib/common.js';
import { isString, getNameByKeyValue } from '@/lib/common.js';
import logger from '@/lib/logger.js';
import {
getCurrentUnixTime,
parseDateFromUnixTime,
@@ -406,6 +457,7 @@ export default {
'initType',
'initCategoryId',
'initAccountId',
'initAmountFilter',
'initKeyword'
],
data() {
@@ -421,8 +473,12 @@ export default {
searchKeyword: '',
customMinDatetime: 0,
customMaxDatetime: 0,
currentAmountFilterType: '',
currentAmountFilterValue1: '0',
currentAmountFilterValue2: '0',
currentPageTransactions: [],
categoryMenuState: false,
amountMenuState: false,
alwaysShowNav: mdAndUp.value,
showNav: mdAndUp.value,
showCustomDateRangeDialog: false,
@@ -497,6 +553,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(' ~ ');
},
queryMonthlyData() {
return isDateRangeMatchOneMonth(this.query.minTime, this.query.maxTime);
},
@@ -597,6 +672,9 @@ export default {
return null;
}
},
allAmountFilterTypes() {
return numeralConstants.allAmountFilterTypeArray;
},
allTransactionTypes() {
return transactionConstants.allTransactionTypes;
},
@@ -638,6 +716,7 @@ export default {
type: this.initType,
categoryId: this.initCategoryId,
accountId: this.initAccountId,
amountFilter: this.initAmountFilter,
keyword: this.initKeyword
});
},
@@ -666,6 +745,7 @@ export default {
type: to.query.type,
categoryId: to.query.categoryId,
accountId: to.query.accountId,
amountFilter: to.query.amountFilter,
keyword: to.query.keyword
});
}
@@ -691,10 +771,12 @@ export default {
type: parseInt(query.type) > 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;
}
@@ -0,0 +1,180 @@
<template>
<f7-page @page:afterin="onPageAfterIn">
<f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Filter Amount')"></f7-nav-title>
<f7-nav-right>
<f7-link :text="$t('Apply')" @click="confirm"></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-list form strong inset dividers class="margin-vertical">
<f7-list-item
class="ebk-small-amount"
link="#" no-chevron
:header="amount1Header"
:title="getDisplayAmount(amount1)"
@click="showAmount1Sheet = true"
>
<number-pad-sheet :min-value="allowedMinAmount"
:max-value="allowedMaxAmount"
v-model:show="showAmount1Sheet"
v-model="amount1"
></number-pad-sheet>
</f7-list-item>
<f7-list-item
class="ebk-small-amount"
link="#" no-chevron
:header="amount2Header"
:title="getDisplayAmount(amount2)"
@click="showAmount2Sheet = true"
v-if="amountCount === 2"
>
<number-pad-sheet :min-value="allowedMinAmount"
:max-value="allowedMaxAmount"
v-model:show="showAmount2Sheet"
v-model="amount2"
></number-pad-sheet>
</f7-list-item>
</f7-list>
<f7-list form strong inset dividers class="margin-vertical">
<f7-list-item :key="filterType.type" :title="$t(filterType.name)"
v-for="filterType in allAmountFilterTypes"
@click="type = filterType.type">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="type === filterType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-page>
</template>
<script>
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
import { useTransactionsStore } from '@/stores/transaction.js';
import numeralConstants from '@/consts/numeral.js';
import transactionConstants from '@/consts/transaction.js';
import { isString } from '@/lib/common.js';
import logger from '@/lib/logger.js';
export default {
props: [
'f7route',
'f7router'
],
data() {
return {
type: '',
amount1: 0,
amount2: 0,
showAmount1Sheet: false,
showAmount2Sheet: false
}
},
computed: {
...mapStores(useSettingsStore, useUserStore, useTransactionsStore),
allAmountFilterTypes() {
return numeralConstants.allAmountFilterTypeArray;
},
allowedMinAmount() {
return transactionConstants.minAmountNumber;
},
allowedMaxAmount() {
return transactionConstants.maxAmountNumber;
},
amountCount() {
return this.getAmountFilterParameterCount(this.type);
},
title() {
const amountFilterType = numeralConstants.allAmountFilterTypeMap[this.type];
return amountFilterType ? this.$t(amountFilterType.name) : this.$t('Amount');
},
amount1Header() {
if (this.type === numeralConstants.allAmountFilterType.GreaterThan.type
|| this.type === numeralConstants.allAmountFilterType.Between.type
|| this.type === numeralConstants.allAmountFilterType.NotBetween.type) {
return this.$t('Min Amount');
} else if (this.type === numeralConstants.allAmountFilterType.LessThan.type) {
return this.$t('Max Amount');
} else {
return this.$t('Amount');
}
},
amount2Header() {
if (this.type === numeralConstants.allAmountFilterType.Between.type) {
return this.$t('Max Amount');
} else if (this.type === numeralConstants.allAmountFilterType.NotBetween.type) {
return this.$t('Max Amount');
} else {
return this.$t('Amount');
}
}
},
created() {
const query = this.f7route.query;
this.type = query.type;
let amount1 = 0, amount2 = 0;
if (isString(query.value)) {
try {
const filterItems = query.value.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 ' + query.value);
}
}
this.amount1 = amount1;
this.amount2 = amount2;
},
methods: {
onPageAfterIn() {
this.$routeBackOnError(this.f7router, 'loadingError');
},
confirm() {
const router = this.f7router;
let amountFilter = this.type;
if (this.amountCount === 1) {
amountFilter += ':' + this.amount1;
} else if (this.amountCount === 2) {
if (this.amount2 < this.amount1) {
this.$toast('Incorrect amount range');
return;
}
amountFilter += ':' + this.amount1 + ':' + this.amount2;
} else {
router.back();
return;
}
this.transactionsStore.updateTransactionListFilter({
amountFilter: amountFilter
});
this.transactionsStore.updateTransactionListInvalidState(true);
router.back();
},
getDisplayAmount(value) {
return this.$locale.formatAmountWithCurrency(this.settingsStore, this.userStore, value, false);
},
getAmountFilterParameterCount(filterType) {
const amountFilterType = numeralConstants.allAmountFilterTypeMap[filterType];
return amountFilterType ? amountFilterType.paramCount : 0;
}
}
}
</script>
+63 -2
View File
@@ -42,7 +42,7 @@
<span :class="{ 'tabbar-item-changed': query.accountId > 0 }">{{ queryAccountName }}</span>
</f7-link>
<f7-link popover-open=".more-popover-menu">
<f7-icon f7="ellipsis_vertical" :class="{ 'tabbar-item-changed': query.type > 0 }"></f7-icon>
<f7-icon f7="ellipsis_vertical" :class="{ 'tabbar-item-changed': query.type > 0 || query.amountFilter }"></f7-icon>
</f7-link>
</f7-toolbar>
@@ -403,6 +403,23 @@
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.type === 4"></f7-icon>
</template>
</f7-list-item>
<f7-list-item group-title :title="$t('Amount')" />
<f7-list-item :class="{ 'list-item-selected': !query.amountFilter }" :title="$t('All')" @click="changeAmountFilter('')">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.amountFilter"></f7-icon>
</template>
</f7-list-item>
<f7-list-item :key="filterType.type"
:class="{ 'list-item-selected': query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`) }"
:title="$t(filterType.name)"
v-for="filterType in allAmountFilterTypes"
@click="changeAmountFilter(filterType.type)">
<template #after>
<span class="margin-right-half" v-if="query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`)">{{ queryAmount }}</span>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.amountFilter && query.amountFilter.startsWith(`${filterType.type}:`)"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
@@ -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;
}