From 5a211c4666db65cbe7025cabd099c9ba90f94944 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Fri, 27 Nov 2020 00:40:43 +0800 Subject: [PATCH] add category api --- cmd/database.go | 2 +- cmd/webserver.go | 9 + pkg/api/transaction_categories.go | 291 +++++++++++++++++++++++++ pkg/errs/transaction_category.go | 9 + pkg/models/transaction_category.go | 115 ++++++++++ pkg/services/transaction_categories.go | 244 +++++++++++++++++++++ src/lib/services.js | 42 ++++ src/locales/en.js | 4 + src/locales/zh_Hans.js | 4 + 9 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 pkg/api/transaction_categories.go create mode 100644 pkg/errs/transaction_category.go create mode 100644 pkg/models/transaction_category.go create mode 100644 pkg/services/transaction_categories.go diff --git a/cmd/database.go b/cmd/database.go index f4987c7d..ad0c6101 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -31,7 +31,7 @@ func updateDatabaseStructure(c *cli.Context) error { _ = datastore.Container.UserStore.SyncStructs(new(models.User), new(models.TwoFactor), new(models.TwoFactorRecoveryCode)) _ = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord)) - _ = datastore.Container.UserDataStore.SyncStructs(new(models.Account)) + _ = datastore.Container.UserDataStore.SyncStructs(new(models.Account), new(models.TransactionCategory)) log.BootInfof("[database.updateDatabaseStructure] maintained successfully") diff --git a/cmd/webserver.go b/cmd/webserver.go index 9ef86b08..a5ec3740 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -176,6 +176,15 @@ func startWebServer(c *cli.Context) error { apiV1Route.POST("/accounts/move.json", bindApi(api.Accounts.AccountMoveHandler)) apiV1Route.POST("/accounts/delete.json", bindApi(api.Accounts.AccountDeleteHandler)) + // Transaction Categories + apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler)) + apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler)) + apiV1Route.POST("/transaction/categories/add.json", bindApi(api.TransactionCategories.CategoryCreateHandler)) + apiV1Route.POST("/transaction/categories/modify.json", bindApi(api.TransactionCategories.CategoryModifyHandler)) + apiV1Route.POST("/transaction/categories/hide.json", bindApi(api.TransactionCategories.CategoryHideHandler)) + apiV1Route.POST("/transaction/categories/move.json", bindApi(api.TransactionCategories.CategoryMoveHandler)) + apiV1Route.POST("/transaction/categories/delete.json", bindApi(api.TransactionCategories.CategoryDeleteHandler)) + // Exchange Rates apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler)) } diff --git a/pkg/api/transaction_categories.go b/pkg/api/transaction_categories.go new file mode 100644 index 00000000..62a01ed3 --- /dev/null +++ b/pkg/api/transaction_categories.go @@ -0,0 +1,291 @@ +package api + +import ( + "sort" + + "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" +) + +type TransactionCategoriesApi struct { + categories *services.TransactionCategoryService +} + +var ( + TransactionCategories = &TransactionCategoriesApi{ + categories: services.TransactionCategories, + } +) + +func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (interface{}, *errs.Error) { + var categoryListReq models.TransactionCategoryListRequest + err := c.ShouldBindQuery(&categoryListReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryListHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + categories, err := a.categories.GetAllCategoriesByUid(uid, categoryListReq.Type, categoryListReq.ParentId) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + categoryResps := make([]*models.TransactionCategoryInfoResponse, len(categories)) + categoryRespMap := make(map[int64]*models.TransactionCategoryInfoResponse) + + for i := 0; i < len(categories); i++ { + categoryResps[i] = categories[i].ToTransactionCategoryInfoResponse() + categoryRespMap[categoryResps[i].Id] = categoryResps[i] + } + + for i := 0; i < len(categoryResps); i++ { + categoryResp := categoryResps[i] + + if categoryResp.ParentId <= models.TRANSACTION_PARENT_ID_LEVEL_ONE { + continue + } + + parentCategory, parentExists := categoryRespMap[categoryResp.ParentId] + + if !parentExists || parentCategory == nil { + continue + } + + parentCategory.SubCategories = append(parentCategory.SubCategories, categoryResp) + } + + finalCategoryResps := make(models.TransactionCategoryInfoResponseSlice, 0) + + for i := 0; i < len(categoryResps); i++ { + if categoryListReq.ParentId <= 0 && categoryResps[i].ParentId == models.TRANSACTION_PARENT_ID_LEVEL_ONE { + sort.Sort(categoryResps[i].SubCategories) + finalCategoryResps = append(finalCategoryResps, categoryResps[i]) + } else if categoryListReq.ParentId > 0 && categoryResps[i].ParentId == categoryListReq.ParentId { + finalCategoryResps = append(finalCategoryResps, categoryResps[i]) + } + } + + sort.Sort(finalCategoryResps) + + typeCategoryMapResponse := make(map[models.TransactionCategoryType]models.TransactionCategoryInfoResponseSlice) + + for i := 0; i < len(finalCategoryResps); i++ { + category := finalCategoryResps[i] + categoryList, _ := typeCategoryMapResponse[category.Type] + + categoryList = append(categoryList, category) + typeCategoryMapResponse[category.Type] = categoryList + } + + return typeCategoryMapResponse, nil +} + +func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (interface{}, *errs.Error) { + var categoryGetReq models.TransactionCategoryGetRequest + err := c.ShouldBindQuery(&categoryGetReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryGetHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + category, err := a.categories.GetCategoryByCategoryId(uid, categoryGetReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryGetHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryGetReq.Id, uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + categoryResp := category.ToTransactionCategoryInfoResponse() + + return categoryResp, nil +} + +func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (interface{}, *errs.Error) { + var categoryCreateReq models.TransactionCategoryCreateRequest + err := c.ShouldBindJSON(&categoryCreateReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + if categoryCreateReq.Type < models.CATEGORY_TYPE_INCOME || categoryCreateReq.Type > models.CATEGORY_TYPE_TRANSFER { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] category type invalid, type is %d", categoryCreateReq.Type) + return nil, errs.ErrTransactionCategoryTypeInvalid + } + + uid := c.GetCurrentUid() + + var maxOrderId = 0 + + if categoryCreateReq.ParentId <= 0 { + maxOrderId, err = a.categories.GetMaxDisplayOrder(uid, categoryCreateReq.Type) + } else { + maxOrderId, err = a.categories.GetMaxSubCategoryDisplayOrder(uid, categoryCreateReq.Type, categoryCreateReq.ParentId) + } + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + category := a.createNewCategory(uid, &categoryCreateReq, maxOrderId+1) + + err = a.categories.CreateCategory(category) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to create category \"id:%d\" for user \"uid:%d\", because %s", category.CategoryId, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId) + + categoryResp := category.ToTransactionCategoryInfoResponse() + + return categoryResp, nil +} + +func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (interface{}, *errs.Error) { + var categoryModifyReq models.TransactionCategoryModifyRequest + err := c.ShouldBindJSON(&categoryModifyReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + category, err := a.categories.GetCategoryByCategoryId(uid, categoryModifyReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + newCategory := &models.TransactionCategory{ + CategoryId: category.CategoryId, + Uid: uid, + Name: categoryModifyReq.Name, + Icon: categoryModifyReq.Icon, + Color: categoryModifyReq.Color, + Comment: categoryModifyReq.Comment, + Hidden: categoryModifyReq.Hidden, + } + + if newCategory.Name == category.Name && + newCategory.Icon == category.Icon && + newCategory.Color == category.Color && + newCategory.Comment == category.Comment && + newCategory.Hidden == category.Hidden { + return nil, errs.ErrNothingWillBeUpdated + } + + err = a.categories.ModifyCategory(newCategory) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to update category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id) + + return true, nil +} + +func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (interface{}, *errs.Error) { + var categoryHideReq models.TransactionCategoryHideRequest + err := c.ShouldBindJSON(&categoryHideReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryHideHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.categories.HideCategory(uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryHideHandler] failed to hide category \"id:%d\" for user \"uid:%d\", because %s", categoryHideReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_categories.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, categoryHideReq.Id) + return true, nil +} + +func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (interface{}, *errs.Error) { + var categoryMoveReq models.TransactionCategoryMoveRequest + err := c.ShouldBindJSON(&categoryMoveReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + categories := make([]*models.TransactionCategory, len(categoryMoveReq.NewDisplayOrders)) + + for i := 0; i < len(categoryMoveReq.NewDisplayOrders); i++ { + newDisplayOrder := categoryMoveReq.NewDisplayOrders[i] + category := &models.TransactionCategory{ + Uid: uid, + CategoryId: newDisplayOrder.Id, + DisplayOrder: newDisplayOrder.DisplayOrder, + } + + categories[i] = category + } + + err = a.categories.ModifyCategoryDisplayOrders(uid, categories) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_categories.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid) + return true, nil +} + +func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (interface{}, *errs.Error) { + var categoryDeleteReq models.TransactionCategoryDeleteRequest + err := c.ShouldBindJSON(&categoryDeleteReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.categories.DeleteCategories(uid, []int64{categoryDeleteReq.Id}) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] failed to delete category \"id:%d\" for user \"uid:%d\", because %s", categoryDeleteReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] user \"uid:%d\" has deleted category \"id:%d\"", uid, categoryDeleteReq.Id) + return true, nil +} + +func (a *TransactionCategoriesApi) createNewCategory(uid int64, categoryCreateReq *models.TransactionCategoryCreateRequest, order int) *models.TransactionCategory { + return &models.TransactionCategory{ + Uid: uid, + Name: categoryCreateReq.Name, + Type: categoryCreateReq.Type, + ParentCategoryId: categoryCreateReq.ParentId, + DisplayOrder: order, + Icon: categoryCreateReq.Icon, + Color: categoryCreateReq.Color, + Comment: categoryCreateReq.Comment, + } +} diff --git a/pkg/errs/transaction_category.go b/pkg/errs/transaction_category.go new file mode 100644 index 00000000..e7503d1e --- /dev/null +++ b/pkg/errs/transaction_category.go @@ -0,0 +1,9 @@ +package errs + +import "net/http" + +var ( + ErrTransactionCategoryIdInvalid = NewNormalError(NORMAL_SUBCATEGORY_CATEGORY, 0, http.StatusBadRequest, "transaction category id is invalid") + ErrTransactionCategoryNotFound = NewNormalError(NORMAL_SUBCATEGORY_CATEGORY, 1, http.StatusBadRequest, "transaction category not found") + ErrTransactionCategoryTypeInvalid = NewNormalError(NORMAL_SUBCATEGORY_CATEGORY, 2, http.StatusBadRequest, "transaction category type is invalid") +) diff --git a/pkg/models/transaction_category.go b/pkg/models/transaction_category.go new file mode 100644 index 00000000..3a1ca61b --- /dev/null +++ b/pkg/models/transaction_category.go @@ -0,0 +1,115 @@ +package models + +// Level-One Transaction Category +const TRANSACTION_PARENT_ID_LEVEL_ONE = 0 + +type TransactionCategoryType byte + +const ( + CATEGORY_TYPE_INCOME TransactionCategoryType = 1 + CATEGORY_TYPE_EXPENSE TransactionCategoryType = 2 + CATEGORY_TYPE_TRANSFER TransactionCategoryType = 3 +) + +type TransactionCategory struct { + CategoryId int64 `xorm:"PK"` + Uid int64 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"` + Deleted bool `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"` + Type TransactionCategoryType `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"` + ParentCategoryId int64 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"` + Name string `xorm:"VARCHAR(32) NOT NULL"` + DisplayOrder int `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"` + Icon int64 `xorm:"NOT NULL"` + Color string `xorm:"VARCHAR(6) NOT NULL"` + Hidden bool `xorm:"NOT NULL"` + Comment string `xorm:"VARCHAR(255) NOT NULL"` + CreatedUnixTime int64 + UpdatedUnixTime int64 + DeletedUnixTime int64 +} + +type TransactionCategoryCreateRequest struct { + Name string `json:"name" binding:"required,notBlank,max=32"` + Type TransactionCategoryType `json:"type" binding:"required"` + ParentId int64 `json:"parentId,string" binding:"min=0"` + Icon int64 `json:"icon,string" binding:"min=1"` + Color string `json:"color" binding:"required,len=6,validHexRGBColor"` + Comment string `json:"comment" binding:"max=255"` +} + +type TransactionCategoryListRequest struct { + Type TransactionCategoryType `form:"type" binding:"min=0"` + ParentId int64 `form:"parent_id,string,default=-1" binding:"min=-1"` +} + +type TransactionCategoryGetRequest struct { + Id int64 `form:"id,string" binding:"required,min=1"` +} + +type TransactionCategoryModifyRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + Name string `json:"name" binding:"required,notBlank,max=32"` + Icon int64 `json:"icon,string" binding:"min=1"` + Color string `json:"color" binding:"required,len=6,validHexRGBColor"` + Comment string `json:"comment" binding:"max=255"` + Hidden bool `json:"hidden"` +} + +type TransactionCategoryHideRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + Hidden bool `json:"hidden"` +} + +type TransactionCategoryMoveRequest struct { + NewDisplayOrders []*TransactionCategoryNewDisplayOrderRequest `json:"newDisplayOrders"` +} + +type TransactionCategoryNewDisplayOrderRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + DisplayOrder int `json:"displayOrder"` +} + +type TransactionCategoryDeleteRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` +} + +type TransactionCategoryInfoResponse struct { + Id int64 `json:"id,string"` + Name string `json:"name"` + ParentId int64 `json:"parentId,string"` + Type TransactionCategoryType `json:"type"` + Icon int64 `json:"icon,string"` + Color string `json:"color"` + Comment string `json:"comment"` + DisplayOrder int `json:"displayOrder"` + Hidden bool `json:"hidden"` + SubCategories TransactionCategoryInfoResponseSlice `json:"subCategories,omitempty"` +} + +func (c *TransactionCategory) ToTransactionCategoryInfoResponse() *TransactionCategoryInfoResponse { + return &TransactionCategoryInfoResponse{ + Id: c.CategoryId, + Name: c.Name, + ParentId: c.ParentCategoryId, + Type: c.Type, + Icon: c.Icon, + Color: c.Color, + Comment: c.Comment, + DisplayOrder: c.DisplayOrder, + Hidden: c.Hidden, + } +} + +type TransactionCategoryInfoResponseSlice []*TransactionCategoryInfoResponse + +func (c TransactionCategoryInfoResponseSlice) Len() int { + return len(c) +} + +func (c TransactionCategoryInfoResponseSlice) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +func (c TransactionCategoryInfoResponseSlice) Less(i, j int) bool { + return c[i].DisplayOrder < c[j].DisplayOrder +} diff --git a/pkg/services/transaction_categories.go b/pkg/services/transaction_categories.go new file mode 100644 index 00000000..d9a533d2 --- /dev/null +++ b/pkg/services/transaction_categories.go @@ -0,0 +1,244 @@ +package services + +import ( + "time" + + "xorm.io/xorm" + + "github.com/mayswind/lab/pkg/datastore" + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/models" + "github.com/mayswind/lab/pkg/uuid" +) + +type TransactionCategoryService struct { + ServiceUsingDB + ServiceUsingUuid +} + +var ( + TransactionCategories = &TransactionCategoryService{ + ServiceUsingDB: ServiceUsingDB{ + container: datastore.Container, + }, + ServiceUsingUuid: ServiceUsingUuid{ + container: uuid.Container, + }, + } +) + +func (s *TransactionCategoryService) GetAllCategoriesByUid(uid int64, categoryType models.TransactionCategoryType, parentCategoryId int64) ([]*models.TransactionCategory, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + condition := "uid=? AND deleted=?" + var conditionParams []interface{} + conditionParams = append(conditionParams, uid) + conditionParams = append(conditionParams, false) + + if categoryType > 0 { + condition = condition + " AND type=?" + conditionParams = append(conditionParams, categoryType) + } + + if parentCategoryId >= 0 { + condition = condition + " AND parent_category_id=?" + conditionParams = append(conditionParams, parentCategoryId) + } + + var categories []*models.TransactionCategory + err := s.UserDataDB(uid).Where(condition, conditionParams...).OrderBy("type asc, parent_category_id asc, display_order asc").Find(&categories) + + return categories, err +} + +func (s *TransactionCategoryService) GetCategoryByCategoryId(uid int64, categoryId int64) (*models.TransactionCategory, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if categoryId <= 0 { + return nil, errs.ErrTransactionCategoryIdInvalid + } + + category := &models.TransactionCategory{} + has, err := s.UserDataDB(uid).Where("uid=? AND deleted=? AND category_id=?", uid, false, categoryId).Get(category) + + if err != nil { + return nil, err + } + + if has { + return category, nil + } else { + return nil, nil + } +} + +func (s *TransactionCategoryService) GetCategoryAndSubCategoriesByCategoryId(uid int64, categoryId int64) ([]*models.TransactionCategory, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if categoryId <= 0 { + return nil, errs.ErrTransactionCategoryIdInvalid + } + + var categories []*models.TransactionCategory + err := s.UserDataDB(uid).Where("uid=? AND deleted=? AND (category_id=? OR parent_category_id=?)", uid, false, categoryId, categoryId).OrderBy("type asc, parent_category_id asc, display_order asc").Find(&categories) + + return categories, err +} + +func (s *TransactionCategoryService) GetMaxDisplayOrder(uid int64, categoryType models.TransactionCategoryType) (int, error) { + if uid <= 0 { + return 0, errs.ErrUserIdInvalid + } + + category := &models.TransactionCategory{} + has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_category_id", "display_order").Where("uid=? AND deleted=? AND type=? AND parent_category_id=?", uid, false, categoryType, models.TRANSACTION_PARENT_ID_LEVEL_ONE).OrderBy("display_order desc").Limit(1).Get(category) + + if err != nil { + return 0, err + } + + if has { + return category.DisplayOrder, nil + } else { + return 0, nil + } +} + +func (s *TransactionCategoryService) GetMaxSubCategoryDisplayOrder(uid int64, categoryType models.TransactionCategoryType, parentCategoryId int64) (int, error) { + if uid <= 0 { + return 0, errs.ErrUserIdInvalid + } + + if parentCategoryId <= 0 { + return 0, errs.ErrTransactionCategoryIdInvalid + } + + category := &models.TransactionCategory{} + has, err := s.UserDataDB(uid).Cols("uid", "deleted", "parent_category_id", "display_order").Where("uid=? AND deleted=? AND type=? AND parent_category_id=?", uid, false, categoryType, parentCategoryId).OrderBy("display_order desc").Limit(1).Get(category) + + if err != nil { + return 0, err + } + + if has { + return category.DisplayOrder, nil + } else { + return 0, nil + } +} + +func (s *TransactionCategoryService) CreateCategory(category *models.TransactionCategory) error { + if category.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + category.CategoryId = s.GenerateUuid(uuid.UUID_TYPE_CATEGORY) + + category.Deleted = false + category.CreatedUnixTime = time.Now().Unix() + category.UpdatedUnixTime = time.Now().Unix() + + return s.UserDataDB(category.Uid).DoTransaction(func(sess *xorm.Session) error { + _, err := sess.Insert(category) + return err + }) +} + +func (s *TransactionCategoryService) ModifyCategory(category *models.TransactionCategory) error { + if category.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + category.UpdatedUnixTime = time.Now().Unix() + + return s.UserDataDB(category.Uid).DoTransaction(func(sess *xorm.Session) error { + updatedRows, err := sess.Cols("name", "icon", "color", "comment", "hidden", "updated_unix_time").Where("category_id=? AND uid=? AND deleted=?", category.CategoryId, category.Uid, false).Update(category) + + if err != nil { + return errs.ErrDatabaseOperationFailed + } + + if updatedRows < 1 { + return errs.ErrTransactionCategoryNotFound + } + + return nil + }) +} + +func (s *TransactionCategoryService) HideCategory(uid int64, ids []int64, hidden bool) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.TransactionCategory{ + Hidden: hidden, + UpdatedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error { + deletedRows, err := sess.Cols("hidden", "updated_unix_time").In("category_id", ids).Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if deletedRows < 1 { + return errs.ErrTransactionCategoryNotFound + } + + return err + }) +} + +func (s *TransactionCategoryService) ModifyCategoryDisplayOrders(uid int64, categories []*models.TransactionCategory) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + for i := 0; i < len(categories); i++ { + categories[i].UpdatedUnixTime = time.Now().Unix() + } + + return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error { + for i := 0; i < len(categories); i++ { + category := categories[i] + _, err := sess.Cols("display_order", "updated_unix_time").Where("category_id=? AND uid=? AND deleted=?", category.CategoryId, uid, false).Update(category) + + if err != nil { + return err + } + } + + return nil + }) +} + +func (s *TransactionCategoryService) DeleteCategories(uid int64, ids []int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.TransactionCategory{ + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(func(sess *xorm.Session) error { + deletedRows, err := sess.Cols("deleted", "deleted_unix_time").In("category_id", ids).Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if deletedRows < 1 { + return errs.ErrTransactionCategoryNotFound + } + + _, err = sess.Cols("deleted", "deleted_unix_time").In("parent_category_id", ids).Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + return err + }) +} diff --git a/src/lib/services.js b/src/lib/services.js index 603c1a57..26a710d0 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -212,6 +212,48 @@ export default { id }); }, + getAllTransactionCategories: ({ type, parentId }) => { + return axios.get('v1/transaction/categories/list.json?type=' + (type || '0') + '&parent_id=' + (parentId || parentId === 0 ? parentId : '-1')); + }, + getTransactionCategory: ({ id }) => { + return axios.get('v1/transaction/categories/get.json?id=' + id); + }, + addTransactionCategory: ({ name, type, parentId, icon, color, comment }) => { + return axios.post('v1/transaction/categories/add.json', { + name, + type, + parentId, + icon, + color, + comment + }); + }, + modifyTransactionCategory: ({ id, name, icon, color, comment, hidden }) => { + return axios.post('v1/transaction/categories/modify.json', { + id, + name, + icon, + color, + comment, + hidden + }); + }, + hideTransactionCategory: ({ id, hidden }) => { + return axios.post('v1/transaction/categories/hide.json', { + id, + hidden + }); + }, + moveTransactionCategory: ({ newDisplayOrders }) => { + return axios.post('v1/transaction/categories/move.json', { + newDisplayOrders, + }); + }, + deleteTransactionCategory: ({ id }) => { + return axios.post('v1/transaction/categories/delete.json', { + id + }); + }, getLatestExchangeRates: () => { return axios.get('v1/exchange_rates/latest.json'); }, diff --git a/src/locales/en.js b/src/locales/en.js index 34546d01..5ed21df8 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -221,6 +221,9 @@ export default { 'sub account category not equals to parent': 'Sub account category does not equal to parent', 'sub account type invalid': 'Sub account type is invalid', 'cannot add or delete sub accounts when modify account': 'You cannot add or delete sub accounts when modify account', + 'transaction category id is invalid': 'Transaction category ID is invalid', + 'transaction category not found': 'Transaction category is not found', + 'transaction category type is invalid': 'Transaction category type is invalid', }, 'parameter': { 'id': 'ID', @@ -236,6 +239,7 @@ export default { 'type': 'Type', 'color': 'Color', 'currency': 'Currency', + 'parentId': 'Parent Node ID', 'comment': 'Comment', }, 'parameterizedError': { diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 86f415e8..8b6f331c 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -221,6 +221,9 @@ export default { 'sub account category not equals to parent': '子账户类别与父账户不同', 'sub account type invalid': '子账户类型无效', 'cannot add or delete sub accounts when modify account': '您不能在修改账户时添加或删除子账户', + 'transaction category id is invalid': '交易分类ID无效', + 'transaction category not found': '交易分类不存在', + 'transaction category type is invalid': '交易分类类型无效', }, 'parameter': { 'id': 'ID', @@ -236,6 +239,7 @@ export default { 'type': '类型', 'color': '颜色', 'currency': '货币', + 'parentId': '父节点ID', 'comment': '备注', }, 'parameterizedError': {