diff --git a/cmd/webserver.go b/cmd/webserver.go index 973ee8d0..b1cf9c80 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -191,9 +191,6 @@ func startWebServer(c *cli.Context) error { // Data apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler)) - // Overview - apiV1Route.GET("/overviews/transaction.json", bindApi(api.Overviews.TransactionOverviewHandler)) - // Accounts apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler)) apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler)) @@ -208,6 +205,7 @@ func startWebServer(c *cli.Context) error { apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler)) apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler)) apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler)) + apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler)) apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler)) apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler)) apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler)) diff --git a/pkg/api/overviews.go b/pkg/api/overviews.go deleted file mode 100644 index 14aa66f5..00000000 --- a/pkg/api/overviews.go +++ /dev/null @@ -1,164 +0,0 @@ -package api - -import ( - "strings" - - "github.com/mayswind/lab/pkg/core" - "github.com/mayswind/lab/pkg/errs" - "github.com/mayswind/lab/pkg/log" - "github.com/mayswind/lab/pkg/models" - "github.com/mayswind/lab/pkg/services" - "github.com/mayswind/lab/pkg/utils" -) - -// OverviewApi represents overview api -type OverviewApi struct { - transactions *services.TransactionService - accounts *services.AccountService -} - -// Initialize an overview api singleton instance -var ( - Overviews = &OverviewApi{ - transactions: services.Transactions, - accounts: services.Accounts, - } -) - -// TransactionOverviewHandler returns transaction overview of current user -func (a *OverviewApi) TransactionOverviewHandler(c *core.Context) (interface{}, *errs.Error) { - var transactionOverviewReq models.TransactionOverviewRequest - err := c.ShouldBindQuery(&transactionOverviewReq) - - if err != nil { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] parse request failed, because %s", err.Error()) - return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) - } - - items := strings.Split(transactionOverviewReq.Query, "|") - requestItems := make([]*models.TransactionOverviewRequestItem, 0, len(items)) - - for i := 0; i < len(items); i++ { - itemValues := strings.Split(items[i], "_") - - if len(itemValues) != 3 { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] parse request item failed, because its not valid item, content is \"%s\"", items[i]) - continue - } - - startTime, err := utils.StringToInt64(itemValues[1]) - - if err != nil { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] parse request item start time failed, because %s", err.Error()) - continue - } - - endTime, err := utils.StringToInt64(itemValues[2]) - - if err != nil { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] parse request item end time failed, because %s", err.Error()) - continue - } - - requestItem := &models.TransactionOverviewRequestItem{ - Name: itemValues[0], - StartTime: startTime, - EndTime: endTime, - } - - requestItems = append(requestItems, requestItem) - } - - if len(requestItems) < 1 { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] parse request failed, because there are no valid items") - return nil, errs.ErrQueryItemsEmpty - } - - if len(requestItems) > 5 { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] parse request failed, because there are too many items") - return nil, errs.ErrQueryItemsTooMuch - } - - uid := c.GetCurrentUid() - - accounts, err := a.accounts.GetAllAccountsByUid(uid) - accountMap := a.accounts.GetAccountMapByList(accounts) - - if err != nil { - log.ErrorfWithRequestId(c, "[overviews.TransactionOverviewHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error()) - return nil, errs.ErrOperationFailed - } - - overviewResp := make(map[string]*models.TransactionOverviewResponseItem) - - for i := 0; i < len(requestItems); i++ { - requestItem := requestItems[i] - - incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(uid, requestItem.StartTime, requestItem.EndTime) - - if err != nil { - log.ErrorfWithRequestId(c, "[overviews.TransactionOverviewHandler] failed to get transaction overview item for user \"uid:%d\", because %s", uid, err.Error()) - return nil, errs.ErrOperationFailed - } - - amountsMap := make(map[string]*models.TransactionOverviewResponseItemAmount) - - for accountId, incomeAmount := range incomeAmounts { - account, exists := accountMap[accountId] - - if !exists { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] cannot find account for account \"id:%d\" of user \"uid:%d\", because %s", accountId, uid) - continue - } - - totalAmounts, exists := amountsMap[account.Currency] - - if !exists { - totalAmounts = &models.TransactionOverviewResponseItemAmount{ - Currency: account.Currency, - IncomeAmount: 0, - ExpenseAmount: 0, - } - } - - totalAmounts.IncomeAmount += incomeAmount - amountsMap[account.Currency] = totalAmounts - } - - for accountId, expenseAmount := range expenseAmounts { - account, exists := accountMap[accountId] - - if !exists { - log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] cannot find account for account \"id:%d\" of user \"uid:%d\", because %s", accountId, uid) - continue - } - - totalAmounts, exists := amountsMap[account.Currency] - - if !exists { - totalAmounts = &models.TransactionOverviewResponseItemAmount{ - Currency: account.Currency, - IncomeAmount: 0, - ExpenseAmount: 0, - } - } - - totalAmounts.ExpenseAmount += expenseAmount - amountsMap[account.Currency] = totalAmounts - } - - allTotalAmounts := make([]*models.TransactionOverviewResponseItemAmount, 0) - - for _, totalAmounts := range amountsMap { - allTotalAmounts = append(allTotalAmounts, totalAmounts) - } - - overviewResp[requestItem.Name] = &models.TransactionOverviewResponseItem{ - StartTime: requestItem.StartTime, - EndTime: requestItem.EndTime, - Amounts: allTotalAmounts, - } - } - - return overviewResp, nil -} diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index ec0a2b1f..5b70beb0 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -224,6 +224,117 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa return statisticResp, nil } +// TransactionAmountsHandler returns transaction amounts of current user +func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{}, *errs.Error) { + var transactionAmountsReq models.TransactionAmountsRequest + err := c.ShouldBindQuery(&transactionAmountsReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + requestItems, err := transactionAmountsReq.GetTransactionAmountsRequestItems() + + if err != nil { + log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] get request item failed, because %s", err.Error()) + return nil, errs.ErrQueryItemsInvalid + } + + if len(requestItems) < 1 { + log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are no valid items") + return nil, errs.ErrQueryItemsEmpty + } + + if len(requestItems) > 5 { + log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items") + return nil, errs.ErrQueryItemsTooMuch + } + + uid := c.GetCurrentUid() + + accounts, err := a.accounts.GetAllAccountsByUid(uid) + accountMap := a.accounts.GetAccountMapByList(accounts) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + amountsResp := make(map[string]*models.TransactionAmountsResponseItem) + + for i := 0; i < len(requestItems); i++ { + requestItem := requestItems[i] + + incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(uid, requestItem.StartTime, requestItem.EndTime) + + if err != nil { + log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + amountsMap := make(map[string]*models.TransactionAmountsResponseItemAmountInfo) + + for accountId, incomeAmount := range incomeAmounts { + account, exists := accountMap[accountId] + + if !exists { + log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] cannot find account for account \"id:%d\" of user \"uid:%d\", because %s", accountId, uid) + continue + } + + totalAmounts, exists := amountsMap[account.Currency] + + if !exists { + totalAmounts = &models.TransactionAmountsResponseItemAmountInfo{ + Currency: account.Currency, + IncomeAmount: 0, + ExpenseAmount: 0, + } + } + + totalAmounts.IncomeAmount += incomeAmount + amountsMap[account.Currency] = totalAmounts + } + + for accountId, expenseAmount := range expenseAmounts { + account, exists := accountMap[accountId] + + if !exists { + log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] cannot find account for account \"id:%d\" of user \"uid:%d\", because %s", accountId, uid) + continue + } + + totalAmounts, exists := amountsMap[account.Currency] + + if !exists { + totalAmounts = &models.TransactionAmountsResponseItemAmountInfo{ + Currency: account.Currency, + IncomeAmount: 0, + ExpenseAmount: 0, + } + } + + totalAmounts.ExpenseAmount += expenseAmount + amountsMap[account.Currency] = totalAmounts + } + + allTotalAmounts := make([]*models.TransactionAmountsResponseItemAmountInfo, 0) + + for _, totalAmounts := range amountsMap { + allTotalAmounts = append(allTotalAmounts, totalAmounts) + } + + amountsResp[requestItem.Name] = &models.TransactionAmountsResponseItem{ + StartTime: requestItem.StartTime, + EndTime: requestItem.EndTime, + Amounts: allTotalAmounts, + } + } + + return amountsResp, nil +} + // TransactionGetHandler returns one specific transaction of current user func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (interface{}, *errs.Error) { var transactionGetReq models.TransactionGetRequest diff --git a/pkg/errs/error.go b/pkg/errs/error.go index 17192dfd..1d78be07 100644 --- a/pkg/errs/error.go +++ b/pkg/errs/error.go @@ -27,7 +27,6 @@ const ( NormalSubcategoryCategory = 6 NormalSubcategoryTag = 7 NormalSubcategoryDataManagement = 8 - NormalSubcategoryOverview = 9 ) // Error represents the specific error returned to user diff --git a/pkg/errs/global.go b/pkg/errs/global.go index d4ba9d4e..1bc46850 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -16,6 +16,9 @@ var ( ErrPageIndexInvalid = NewNormalError(NormalSubcategoryGlobal, 6, http.StatusBadRequest, "page index is invalid") ErrPageCountInvalid = NewNormalError(NormalSubcategoryGlobal, 7, http.StatusBadRequest, "page count is invalid") ErrClientTimezoneOffsetInvalid = NewNormalError(NormalSubcategoryGlobal, 8, http.StatusBadRequest, "client timezone offset is invalid") + ErrQueryItemsEmpty = NewNormalError(NormalSubcategoryGlobal, 9, http.StatusBadRequest, "query items cannot be empty") + ErrQueryItemsTooMuch = NewNormalError(NormalSubcategoryGlobal, 10, http.StatusBadRequest, "query items too much") + ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item") ) // GetParameterInvalidMessage returns specific error message for invalid parameter error diff --git a/pkg/errs/overview.go b/pkg/errs/overview.go deleted file mode 100644 index 0eb3c35b..00000000 --- a/pkg/errs/overview.go +++ /dev/null @@ -1,9 +0,0 @@ -package errs - -import "net/http" - -// Error codes related to overview -var ( - ErrQueryItemsEmpty = NewNormalError(NormalSubcategoryOverview, 0, http.StatusBadRequest, "query items cannot be empty") - ErrQueryItemsTooMuch = NewNormalError(NormalSubcategoryOverview, 1, http.StatusBadRequest, "query items too much") -) diff --git a/pkg/models/overview.go b/pkg/models/overview.go deleted file mode 100644 index d945a8ef..00000000 --- a/pkg/models/overview.go +++ /dev/null @@ -1,27 +0,0 @@ -package models - -// TransactionOverviewRequest represents all parameters of transaction overview request -type TransactionOverviewRequest struct { - Query string `form:"query"` -} - -// TransactionOverviewRequestItem represents an item of transaction overview request -type TransactionOverviewRequestItem struct { - Name string - StartTime int64 - EndTime int64 -} - -// TransactionOverviewResponseItem represents an item of transaction overview -type TransactionOverviewResponseItem struct { - StartTime int64 `json:"startTime"` - EndTime int64 `json:"endTime"` - Amounts []*TransactionOverviewResponseItemAmount `json:"amounts"` -} - -// TransactionOverviewResponseItemAmount represents amount info for an response item -type TransactionOverviewResponseItemAmount struct { - Currency string `json:"currency"` - IncomeAmount int64 `json:"incomeAmount"` - ExpenseAmount int64 `json:"expenseAmount"` -} diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index f0bfd1b9..85563169 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -1,6 +1,11 @@ package models -import "github.com/mayswind/lab/pkg/utils" +import ( + "strings" + + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/utils" +) // TransactionType represents transaction type type TransactionType byte @@ -125,6 +130,18 @@ type TransactionStatisticRequest struct { EndTime int64 `form:"end_time" binding:"min=0"` } +// TransactionAmountsRequest represents all parameters of transaction amounts request +type TransactionAmountsRequest struct { + Query string `form:"query"` +} + +// TransactionAmountsRequestItem represents an item of transaction amounts request +type TransactionAmountsRequestItem struct { + Name string + StartTime int64 + EndTime int64 +} + // TransactionGetRequest represents all parameters of transaction getting request type TransactionGetRequest struct { Id int64 `form:"id,string" binding:"required,min=1"` @@ -176,7 +193,7 @@ type TransactionInfoPageWrapperResponse2 struct { TotalCount int64 `json:"total_count"` } -// TransactionStatisticResponse represents an item of transaction overview +// TransactionStatisticResponse represents an item of transaction amounts type TransactionStatisticResponse struct { StartTime int64 `json:"startTime"` EndTime int64 `json:"endTime"` @@ -190,6 +207,20 @@ type TransactionStatisticResponseItem struct { TotalAmount int64 `json:"amount"` } +// TransactionAmountsResponseItem represents an item of transaction amounts +type TransactionAmountsResponseItem struct { + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` + Amounts []*TransactionAmountsResponseItemAmountInfo `json:"amounts"` +} + +// TransactionAmountsResponseItemAmountInfo represents amount info for an response item +type TransactionAmountsResponseItemAmountInfo struct { + Currency string `json:"currency"` + IncomeAmount int64 `json:"incomeAmount"` + ExpenseAmount int64 `json:"expenseAmount"` +} + // 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) { @@ -261,6 +292,41 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) * } } +func (t *TransactionAmountsRequest) GetTransactionAmountsRequestItems() ([]*TransactionAmountsRequestItem, error) { + items := strings.Split(t.Query, "|") + requestItems := make([]*TransactionAmountsRequestItem, 0, len(items)) + + for i := 0; i < len(items); i++ { + itemValues := strings.Split(items[i], "_") + + if len(itemValues) != 3 { + return nil, errs.ErrQueryItemsInvalid + } + + startTime, err := utils.StringToInt64(itemValues[1]) + + if err != nil { + return nil, err + } + + endTime, err := utils.StringToInt64(itemValues[2]) + + if err != nil { + return nil, err + } + + requestItem := &TransactionAmountsRequestItem{ + Name: itemValues[0], + StartTime: startTime, + EndTime: endTime, + } + + requestItems = append(requestItems, requestItem) + } + + return requestItems, nil +} + // TransactionInfoResponseSlice represents the slice data structure of TransactionInfoResponse type TransactionInfoResponseSlice []*TransactionInfoResponse diff --git a/src/lib/services.js b/src/lib/services.js index b8312b3c..0c2dc9bf 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -171,27 +171,6 @@ export default { password }); }, - getTransactionOverview: ({ today, thisWeek, thisMonth, thisYear }) => { - const queryParams = []; - - if (today) { - queryParams.push(`today_${today.startTime}_${today.endTime}`); - } - - if (thisWeek) { - queryParams.push(`thisWeek_${thisWeek.startTime}_${thisWeek.endTime}`); - } - - if (thisMonth) { - queryParams.push(`thisMonth_${thisMonth.startTime}_${thisMonth.endTime}`); - } - - if (thisYear) { - queryParams.push(`thisYear_${thisYear.startTime}_${thisYear.endTime}`); - } - - return axios.get('v1/overviews/transaction.json' + (queryParams.length ? '?query=' + queryParams.join('|') : '')); - }, getAllAccounts: ({ visibleOnly }) => { return axios.get('v1/accounts/list.json?visible_only=' + !!visibleOnly); }, @@ -255,6 +234,27 @@ export default { return axios.get('v1/transactions/statistics.json' + (queryParams.length ? '?' + queryParams.join('&') : '')); }, + getTransactionAmounts: ({ today, thisWeek, thisMonth, thisYear }) => { + const queryParams = []; + + if (today) { + queryParams.push(`today_${today.startTime}_${today.endTime}`); + } + + if (thisWeek) { + queryParams.push(`thisWeek_${thisWeek.startTime}_${thisWeek.endTime}`); + } + + if (thisMonth) { + queryParams.push(`thisMonth_${thisMonth.startTime}_${thisMonth.endTime}`); + } + + if (thisYear) { + queryParams.push(`thisYear_${thisYear.startTime}_${thisYear.endTime}`); + } + + return axios.get('v1/transactions/amounts.json' + (queryParams.length ? '?query=' + queryParams.join('|') : '')); + }, getTransaction: ({ id }) => { return axios.get(`v1/transactions/get.json?id=${id}&trim_account=true&trim_category=true&trim_tag=true`); }, diff --git a/src/store/overview.js b/src/store/overview.js index 1cd895b6..290acd14 100644 --- a/src/store/overview.js +++ b/src/store/overview.js @@ -17,7 +17,7 @@ export function loadTransactionOverview(context, { defaultCurrency, dateRange, f } return new Promise((resolve, reject) => { - services.getTransactionOverview({ + services.getTransactionAmounts({ today: dateRange.today, thisWeek: dateRange.thisWeek, thisMonth: dateRange.thisMonth,