From de086aa29e1e39c679def9d127f77af7737dc39a Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 29 Jul 2024 00:53:19 +0800 Subject: [PATCH] add transaction template --- cmd/database.go | 8 + cmd/webserver.go | 10 + pkg/api/data_managements.go | 17 + pkg/api/transaction_templates.go | 333 ++++++++++ .../duplicate_checker_type.go | 1 + pkg/errs/error.go | 1 + pkg/errs/transaction_template.go | 9 + pkg/models/data_management.go | 1 + pkg/models/transaction_template.go | 159 +++++ pkg/services/transaction_templates.go | 261 ++++++++ pkg/utils/datetimes.go | 8 + pkg/uuid/uuid_type.go | 1 + src/consts/template.js | 7 + src/lib/category.js | 8 + src/lib/services.js | 48 ++ src/locales/en.js | 19 + src/locales/zh_Hans.js | 19 + src/router/desktop.js | 7 + src/router/mobile.js | 7 + src/stores/index.js | 4 + src/stores/transactionTemplate.js | 477 ++++++++++++++ src/views/desktop/MainLayout.vue | 8 + src/views/desktop/templates/ListPage.vue | 585 ++++++++++++++++++ .../transactions/list/dialogs/EditDialog.vue | 139 +++-- src/views/mobile/templates/ListPage.vue | 16 + 25 files changed, 2109 insertions(+), 44 deletions(-) create mode 100644 pkg/api/transaction_templates.go create mode 100644 pkg/errs/transaction_template.go create mode 100644 pkg/models/transaction_template.go create mode 100644 pkg/services/transaction_templates.go create mode 100644 src/consts/template.js create mode 100644 src/stores/transactionTemplate.js create mode 100644 src/views/desktop/templates/ListPage.vue create mode 100644 src/views/mobile/templates/ListPage.vue diff --git a/cmd/database.go b/cmd/database.go index 6202b650..781c558c 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -116,5 +116,13 @@ func updateAllDatabaseTablesStructure() error { log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully") + err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTemplate)) + + if err != nil { + return err + } + + log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully") + return nil } diff --git a/cmd/webserver.go b/cmd/webserver.go index 24bda308..7b5feffc 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -320,6 +320,16 @@ func startWebServer(c *cli.Context) error { apiV1Route.POST("/transaction/tags/move.json", bindApi(api.TransactionTags.TagMoveHandler)) apiV1Route.POST("/transaction/tags/delete.json", bindApi(api.TransactionTags.TagDeleteHandler)) + // Transaction Templates + apiV1Route.GET("/transaction/templates/list.json", bindApi(api.TransactionTemplates.TemplateListHandler)) + apiV1Route.GET("/transaction/templates/get.json", bindApi(api.TransactionTemplates.TemplateGetHandler)) + apiV1Route.POST("/transaction/templates/add.json", bindApi(api.TransactionTemplates.TemplateCreateHandler)) + apiV1Route.POST("/transaction/templates/modify_name.json", bindApi(api.TransactionTemplates.TemplateModifyNameHandler)) + apiV1Route.POST("/transaction/templates/modify.json", bindApi(api.TransactionTemplates.TemplateModifyHandler)) + apiV1Route.POST("/transaction/templates/hide.json", bindApi(api.TransactionTemplates.TemplateHideHandler)) + apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler)) + apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler)) + // Exchange Rates apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler)) } diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index 9ecba143..e1c8e91c 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -27,6 +27,7 @@ type DataManagementsApi struct { transactions *services.TransactionService categories *services.TransactionCategoryService tags *services.TransactionTagService + templates *services.TransactionTemplateService } // Initialize a data management api singleton instance @@ -40,6 +41,7 @@ var ( transactions: services.Transactions, categories: services.TransactionCategories, tags: services.TransactionTags, + templates: services.TransactionTemplates, } ) @@ -84,11 +86,19 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs. return nil, errs.ErrOperationFailed } + totalTransactionTemplateCount, err := a.templates.GetTotalTemplateCountByUid(c, uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction template count for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + dataStatisticsResp := &models.DataStatisticsResponse{ TotalAccountCount: totalAccountCount, TotalTransactionCategoryCount: totalTransactionCategoryCount, TotalTransactionTagCount: totalTransactionTagCount, TotalTransactionCount: totalTransactionCount, + TotalTransactionTemplateCount: totalTransactionTemplateCount, } return dataStatisticsResp, nil @@ -140,6 +150,13 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error return nil, errs.Or(err, errs.ErrOperationFailed) } + err = a.templates.DeleteAllTemplates(c, uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction templates, because %s", err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + log.InfofWithRequestId(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid) return true, nil } diff --git a/pkg/api/transaction_templates.go b/pkg/api/transaction_templates.go new file mode 100644 index 00000000..e35f4ba5 --- /dev/null +++ b/pkg/api/transaction_templates.go @@ -0,0 +1,333 @@ +package api + +import ( + "sort" + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/log" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/services" + "github.com/mayswind/ezbookkeeping/pkg/settings" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +// TransactionTemplatesApi represents transaction template api +type TransactionTemplatesApi struct { + templates *services.TransactionTemplateService +} + +// Initialize a transaction template api singleton instance +var ( + TransactionTemplates = &TransactionTemplatesApi{ + templates: services.TransactionTemplates, + } +) + +// TemplateListHandler returns transaction template list of current user +func (a *TransactionTemplatesApi) TemplateListHandler(c *core.Context) (any, *errs.Error) { + var templateListReq models.TransactionTemplateListRequest + err := c.ShouldBindQuery(&templateListReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateListHandler] failed to get templates for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + templateResps := make(models.TransactionTemplateInfoResponseSlice, len(templates)) + serverUtcOffset := utils.GetServerTimezoneOffsetMinutes() + + for i := 0; i < len(templates); i++ { + templateResps[i] = templates[i].ToTransactionTemplateInfoResponse(serverUtcOffset) + } + + sort.Sort(templateResps) + + return templateResps, nil +} + +// TemplateGetHandler returns one specific transaction template of current user +func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.Context) (any, *errs.Error) { + var templateGetReq models.TransactionTemplateGetRequest + err := c.ShouldBindQuery(&templateGetReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.TemplateGetHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + template, err := a.templates.GetTemplateByTemplateId(c, uid, templateGetReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateGetHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateGetReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + serverUtcOffset := utils.GetServerTimezoneOffsetMinutes() + templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset) + + return templateResp, nil +} + +// TemplateCreateHandler saves a new transaction template by request parameters for current user +func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, *errs.Error) { + var templateCreateReq models.TransactionTemplateCreateRequest + err := c.ShouldBindJSON(&templateCreateReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + + maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + serverUtcOffset := utils.GetServerTimezoneOffsetMinutes() + template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1) + + if settings.Container.Current.EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" { + found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId) + + if found { + log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid) + templateId, err := utils.StringToInt64(remark) + + if err == nil { + template, err = a.templates.GetTemplateByTemplateId(c, uid, templateId) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get existed template \"id:%d\" for user \"uid:%d\", because %s", templateId, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset) + + return templateResp, nil + } + } + } + + err = a.templates.CreateTemplate(c, template) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to create template \"id:%d\" for user \"uid:%d\", because %s", template.TemplateId, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId) + + duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId)) + templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset) + + return templateResp, nil +} + +// TemplateModifyNameHandler updates the name of an existed transaction template by request parameters for current user +func (a *TransactionTemplatesApi) TemplateModifyNameHandler(c *core.Context) (any, *errs.Error) { + var templateModifyReq models.TransactionTemplateModifyNameRequest + err := c.ShouldBindJSON(&templateModifyReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + if templateModifyReq.Name == template.Name { + return nil, errs.ErrNothingWillBeUpdated + } + + newTemplate := &models.TransactionTemplate{ + TemplateId: template.TemplateId, + Uid: template.Uid, + Name: templateModifyReq.Name, + } + + err = a.templates.ModifyTemplateName(c, newTemplate) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id) + + serverUtcOffset := utils.GetServerTimezoneOffsetMinutes() + template.Name = newTemplate.Name + templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset) + + return templateResp, nil +} + +// TemplateModifyHandler saves an existed transaction template by request parameters for current user +func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *errs.Error) { + var templateModifyReq models.TransactionTemplateModifyRequest + err := c.ShouldBindJSON(&templateModifyReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + newTemplate := &models.TransactionTemplate{ + TemplateId: template.TemplateId, + Uid: uid, + Type: templateModifyReq.Type, + CategoryId: templateModifyReq.CategoryId, + AccountId: templateModifyReq.SourceAccountId, + TagIds: strings.Join(templateModifyReq.TagIds, ","), + Amount: templateModifyReq.SourceAmount, + RelatedAccountId: templateModifyReq.DestinationAccountId, + RelatedAccountAmount: templateModifyReq.DestinationAmount, + Comment: templateModifyReq.Comment, + } + + if newTemplate.Type == template.Type && + newTemplate.CategoryId == template.CategoryId && + newTemplate.AccountId == template.AccountId && + newTemplate.TagIds == template.TagIds && + newTemplate.Amount == template.Amount && + newTemplate.RelatedAccountId == template.RelatedAccountId && + newTemplate.RelatedAccountAmount == template.RelatedAccountAmount && + newTemplate.Comment == template.Comment { + return nil, errs.ErrNothingWillBeUpdated + } + + err = a.templates.ModifyTemplate(c, newTemplate) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_templates.TemplateModifyHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id) + + serverUtcOffset := utils.GetServerTimezoneOffsetMinutes() + newTemplate.Name = template.Name + newTemplate.DisplayOrder = template.DisplayOrder + newTemplate.Hidden = template.Hidden + templateResp := newTemplate.ToTransactionTemplateInfoResponse(serverUtcOffset) + + return templateResp, nil +} + +// TemplateHideHandler hides an transaction template by request parameters for current user +func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.Context) (any, *errs.Error) { + var templateHideReq models.TransactionTemplateHideRequest + err := c.ShouldBindJSON(&templateHideReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.TemplateHideHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateHideHandler] failed to hide template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_templates.TemplateHideHandler] user \"uid:%d\" has hidden template \"id:%d\"", uid, templateHideReq.Id) + return true, nil +} + +// TemplateMoveHandler moves display order of existed transaction templates by request parameters for current user +func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.Context) (any, *errs.Error) { + var templateMoveReq models.TransactionTemplateMoveRequest + err := c.ShouldBindJSON(&templateMoveReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.CategoryMoveHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders)) + + for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ { + newDisplayOrder := templateMoveReq.NewDisplayOrders[i] + template := &models.TransactionTemplate{ + Uid: uid, + TemplateId: newDisplayOrder.Id, + DisplayOrder: newDisplayOrder.DisplayOrder, + } + + templates[i] = template + } + + err = a.templates.ModifyTemplateDisplayOrders(c, uid, templates) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateMoveHandler] failed to move templates for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_templates.TemplateMoveHandler] user \"uid:%d\" has moved templates", uid) + return true, nil +} + +// TemplateDeleteHandler deletes an existed transaction template by request parameters for current user +func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.Context) (any, *errs.Error) { + var templateDeleteReq models.TransactionTemplateDeleteRequest + err := c.ShouldBindJSON(&templateDeleteReq) + + if err != nil { + log.WarnfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id) + + if err != nil { + log.ErrorfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] failed to delete template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.InfofWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] user \"uid:%d\" has deleted template \"id:%d\"", uid, templateDeleteReq.Id) + return true, nil +} + +func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate { + return &models.TransactionTemplate{ + Uid: uid, + TemplateType: templateCreateReq.TemplateType, + Name: templateCreateReq.Name, + Type: models.TRANSACTION_TYPE_EXPENSE, + DisplayOrder: order, + } +} diff --git a/pkg/duplicatechecker/duplicate_checker_type.go b/pkg/duplicatechecker/duplicate_checker_type.go index 693ccf86..9f5e10bf 100644 --- a/pkg/duplicatechecker/duplicate_checker_type.go +++ b/pkg/duplicatechecker/duplicate_checker_type.go @@ -9,4 +9,5 @@ const ( DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT DuplicateCheckerType = 1 DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2 DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3 + DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4 ) diff --git a/pkg/errs/error.go b/pkg/errs/error.go index 4d49fa4f..d529d779 100644 --- a/pkg/errs/error.go +++ b/pkg/errs/error.go @@ -29,6 +29,7 @@ const ( NormalSubcategoryTag = 7 NormalSubcategoryDataManagement = 8 NormalSubcategoryMapProxy = 9 + NormalSubcategoryTemplate = 10 ) // Error represents the specific error returned to user diff --git a/pkg/errs/transaction_template.go b/pkg/errs/transaction_template.go new file mode 100644 index 00000000..9f150327 --- /dev/null +++ b/pkg/errs/transaction_template.go @@ -0,0 +1,9 @@ +package errs + +import "net/http" + +// Error codes related to transaction templates +var ( + ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid") + ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found") +) diff --git a/pkg/models/data_management.go b/pkg/models/data_management.go index 32948117..c7ab7e8f 100644 --- a/pkg/models/data_management.go +++ b/pkg/models/data_management.go @@ -11,4 +11,5 @@ type DataStatisticsResponse struct { TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"` TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"` TotalTransactionCount int64 `json:"totalTransactionCount,string"` + TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"` } diff --git a/pkg/models/transaction_template.go b/pkg/models/transaction_template.go new file mode 100644 index 00000000..3fe1aaff --- /dev/null +++ b/pkg/models/transaction_template.go @@ -0,0 +1,159 @@ +package models + +import ( + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +// TransactionTemplateType represents transaction template type in database +type TransactionTemplateType byte + +// Transaction template types +const ( + TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1 +) + +// TransactionTemplate represents transaction template stored in database +type TransactionTemplate struct { + TemplateId int64 `xorm:"PK"` + Uid int64 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"` + Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"` + TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"` + Name string `xorm:"VARCHAR(32) NOT NULL"` + Type TransactionType `xorm:"NOT NULL"` + CategoryId int64 `xorm:"NOT NULL"` + AccountId int64 `xorm:"NOT NULL"` + TagIds string `xorm:"VARCHAR(255) NOT NULL"` + Amount int64 `xorm:"NOT NULL"` + RelatedAccountId int64 `xorm:"NOT NULL"` + RelatedAccountAmount int64 `xorm:"NOT NULL"` + Comment string `xorm:"VARCHAR(255) NOT NULL"` + DisplayOrder int32 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"` + Hidden bool `xorm:"NOT NULL"` + CreatedUnixTime int64 + UpdatedUnixTime int64 + DeletedUnixTime int64 +} + +// TransactionTemplateListRequest represents all parameters of transaction template list request +type TransactionTemplateListRequest struct { + TemplateType TransactionTemplateType `form:"templateType" binding:"required,min=1,max=1"` +} + +// TransactionTemplateGetRequest represents all parameters of transaction template getting request +type TransactionTemplateGetRequest struct { + Id int64 `form:"id,string" binding:"required,min=1"` +} + +// TransactionTemplateCreateRequest represents all parameters of transaction template creation request +type TransactionTemplateCreateRequest struct { + TemplateType TransactionTemplateType `json:"templateType" binding:"required,min=1,max=1"` + Name string `json:"name" binding:"required,notBlank,max=32"` + ClientSessionId string `json:"clientSessionId"` +} + +// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request +type TransactionTemplateModifyNameRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + Name string `json:"name" binding:"required,notBlank,max=32"` +} + +// TransactionTemplateModifyRequest represents all parameters of transaction template modification request +type TransactionTemplateModifyRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + Type TransactionType `json:"type" binding:"required"` + CategoryId int64 `json:"categoryId,string"` + SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"` + DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"` + SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"` + DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"` + TagIds []string `json:"tagIds"` + Comment string `json:"comment" binding:"max=255"` +} + +// TransactionTemplateHideRequest represents all parameters of transaction template hiding request +type TransactionTemplateHideRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + Hidden bool `json:"hidden"` +} + +// TransactionTemplateMoveRequest represents all parameters of transaction template moving request +type TransactionTemplateMoveRequest struct { + NewDisplayOrders []*TransactionTemplateNewDisplayOrderRequest `json:"newDisplayOrders"` +} + +// TransactionTemplateNewDisplayOrderRequest represents a data pair of id and display order +type TransactionTemplateNewDisplayOrderRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + DisplayOrder int32 `json:"displayOrder"` +} + +// TransactionTemplateDeleteRequest represents all parameters of transaction template deleting request +type TransactionTemplateDeleteRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` +} + +type TransactionTemplateInfoResponse struct { + *TransactionInfoResponse + TemplateType TransactionTemplateType `json:"templateType"` + Name string `json:"name"` + DisplayOrder int32 `json:"displayOrder"` + Hidden bool `json:"hidden"` +} + +// ToTransactionInfoResponse returns a view-object according to database model +func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse { + tagIds := make([]string, 0, 0) + + if t.TagIds != "" { + tagIds = strings.Split(t.TagIds, ",") + } + + return &TransactionInfoResponse{ + Id: t.TemplateId, + TimeSequenceId: utils.GetMinTransactionTimeFromUnixTime(t.CreatedUnixTime), + Type: t.Type, + CategoryId: t.CategoryId, + Time: 0, + UtcOffset: utcOffset, + SourceAccountId: t.AccountId, + DestinationAccountId: t.RelatedAccountId, + SourceAmount: t.Amount, + DestinationAmount: t.RelatedAccountAmount, + HideAmount: false, + TagIds: tagIds, + Comment: t.Comment, + GeoLocation: nil, + Editable: true, + } +} + +// ToTransactionTemplateInfoResponse returns a view-object according to database model +func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(utcOffset int16) *TransactionTemplateInfoResponse { + return &TransactionTemplateInfoResponse{ + TransactionInfoResponse: t.ToTransactionInfoResponse(utcOffset), + TemplateType: t.TemplateType, + Name: t.Name, + DisplayOrder: t.DisplayOrder, + Hidden: t.Hidden, + } +} + +// TransactionTemplateInfoResponseSlice represents the slice data structure of TransactionTemplateInfoResponse +type TransactionTemplateInfoResponseSlice []*TransactionTemplateInfoResponse + +// Len returns the count of items +func (s TransactionTemplateInfoResponseSlice) Len() int { + return len(s) +} + +// Swap swaps two items +func (s TransactionTemplateInfoResponseSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less reports whether the first item is less than the second one +func (s TransactionTemplateInfoResponseSlice) Less(i, j int) bool { + return s[i].DisplayOrder < s[j].DisplayOrder +} diff --git a/pkg/services/transaction_templates.go b/pkg/services/transaction_templates.go new file mode 100644 index 00000000..0be94591 --- /dev/null +++ b/pkg/services/transaction_templates.go @@ -0,0 +1,261 @@ +package services + +import ( + "time" + "xorm.io/xorm" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/datastore" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/uuid" +) + +// TransactionTemplateService represents transaction template service +type TransactionTemplateService struct { + ServiceUsingDB + ServiceUsingUuid +} + +// Initialize a transaction template service singleton instance +var ( + TransactionTemplates = &TransactionTemplateService{ + ServiceUsingDB: ServiceUsingDB{ + container: datastore.Container, + }, + ServiceUsingUuid: ServiceUsingUuid{ + container: uuid.Container, + }, + } +) + +// GetTotalTemplateCountByUid returns total template count of user +func (s *TransactionTemplateService) GetTotalTemplateCountByUid(c *core.Context, uid int64) (int64, error) { + if uid <= 0 { + return 0, errs.ErrUserIdInvalid + } + + count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Count(&models.TransactionTemplate{}) + + return count, err +} + +// GetAllTemplatesByUid returns all transaction template models of user +func (s *TransactionTemplateService) GetAllTemplatesByUid(c *core.Context, uid int64, templateType models.TransactionTemplateType) ([]*models.TransactionTemplate, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + var templates []*models.TransactionTemplate + err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).Find(&templates) + + return templates, err +} + +// GetTemplateByTemplateId returns a transaction template model according to transaction template id +func (s *TransactionTemplateService) GetTemplateByTemplateId(c *core.Context, uid int64, templateId int64) (*models.TransactionTemplate, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if templateId <= 0 { + return nil, errs.ErrTransactionTemplateIdInvalid + } + + template := &models.TransactionTemplate{} + has, err := s.UserDataDB(uid).NewSession(c).ID(templateId).Where("uid=? AND deleted=?", uid, false).Get(template) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrTransactionTemplateNotFound + } + + return template, nil +} + +// GetMaxDisplayOrder returns the max display order +func (s *TransactionTemplateService) GetMaxDisplayOrder(c *core.Context, uid int64, templateType models.TransactionTemplateType) (int32, error) { + if uid <= 0 { + return 0, errs.ErrUserIdInvalid + } + + template := &models.TransactionTemplate{} + has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).OrderBy("display_order desc").Limit(1).Get(template) + + if err != nil { + return 0, err + } + + if has { + return template.DisplayOrder, nil + } else { + return 0, nil + } +} + +// CreateTemplate saves a new transaction template model to database +func (s *TransactionTemplateService) CreateTemplate(c *core.Context, template *models.TransactionTemplate) error { + if template.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + template.TemplateId = s.GenerateUuid(uuid.UUID_TYPE_TEMPLATE) + + if template.TemplateId < 1 { + return errs.ErrSystemIsBusy + } + + template.Deleted = false + template.CreatedUnixTime = time.Now().Unix() + template.UpdatedUnixTime = time.Now().Unix() + + return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error { + _, err := sess.Insert(template) + return err + }) +} + +// ModifyTemplate saves an existed transaction template model to database +func (s *TransactionTemplateService) ModifyTemplate(c *core.Context, template *models.TransactionTemplate) error { + if template.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + template.UpdatedUnixTime = time.Now().Unix() + + return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error { + updatedRows, err := sess.ID(template.TemplateId).Cols("type", "category_id", "account_id", "tag_ids", "amount", "related_account_id", "related_account_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template) + + if err != nil { + return err + } else if updatedRows < 1 { + return errs.ErrTransactionTemplateNotFound + } + + return err + }) +} + +// ModifyTemplateName updates the name of an existed transaction template model to database +func (s *TransactionTemplateService) ModifyTemplateName(c *core.Context, template *models.TransactionTemplate) error { + if template.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + template.UpdatedUnixTime = time.Now().Unix() + + return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error { + updatedRows, err := sess.ID(template.TemplateId).Cols("name", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template) + + if err != nil { + return err + } else if updatedRows < 1 { + return errs.ErrTransactionTemplateNotFound + } + + return err + }) +} + +// HideTemplate updates hidden field of given transaction templates +func (s *TransactionTemplateService) HideTemplate(c *core.Context, uid int64, ids []int64, hidden bool) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.TransactionTemplate{ + Hidden: hidden, + UpdatedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + updatedRows, err := sess.Cols("hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("template_id", ids).Update(updateModel) + + if err != nil { + return err + } else if updatedRows < 1 { + return errs.ErrTransactionTemplateNotFound + } + + return err + }) +} + +// ModifyTemplateDisplayOrders updates display order of given transaction templates +func (s *TransactionTemplateService) ModifyTemplateDisplayOrders(c *core.Context, uid int64, templates []*models.TransactionTemplate) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + for i := 0; i < len(templates); i++ { + templates[i].UpdatedUnixTime = time.Now().Unix() + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + for i := 0; i < len(templates); i++ { + template := templates[i] + updatedRows, err := sess.ID(template.TemplateId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(template) + + if err != nil { + return err + } else if updatedRows < 1 { + return errs.ErrTransactionTemplateNotFound + } + } + + return nil + }) +} + +// DeleteTemplate deletes an existed transaction template from database +func (s *TransactionTemplateService) DeleteTemplate(c *core.Context, uid int64, templateId int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.TransactionTemplate{ + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + deletedRows, err := sess.ID(templateId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if err != nil { + return err + } else if deletedRows < 1 { + return errs.ErrTransactionTemplateNotFound + } + + return err + }) +} + +// DeleteAllTemplates deletes all existed transaction templates from database +func (s *TransactionTemplateService) DeleteAllTemplates(c *core.Context, uid int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.TransactionTemplate{ + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + _, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if err != nil { + return err + } + + return nil + }) +} diff --git a/pkg/utils/datetimes.go b/pkg/utils/datetimes.go index 0bef0c6b..9ea5897e 100644 --- a/pkg/utils/datetimes.go +++ b/pkg/utils/datetimes.go @@ -178,6 +178,14 @@ func GetTimezoneOffsetMinutes(timezone *time.Location) int16 { return tzMinuteOffset } +// GetServerTimezoneOffsetMinutes returns offset minutes of current server timezone +func GetServerTimezoneOffsetMinutes() int16 { + _, tzOffset := time.Now().Zone() + tzMinuteOffset := int16(tzOffset / 60) + + return tzMinuteOffset +} + // FormatTimezoneOffset returns "+/-HH:MM" format of timezone func FormatTimezoneOffset(timezone *time.Location) string { tzMinutesOffset := GetTimezoneOffsetMinutes(timezone) diff --git a/pkg/uuid/uuid_type.go b/pkg/uuid/uuid_type.go index 0659a176..1ef92afe 100644 --- a/pkg/uuid/uuid_type.go +++ b/pkg/uuid/uuid_type.go @@ -12,4 +12,5 @@ const ( UUID_TYPE_CATEGORY UuidType = 4 UUID_TYPE_TAG UuidType = 5 UUID_TYPE_TAG_INDEX UuidType = 6 + UUID_TYPE_TEMPLATE UuidType = 7 ) diff --git a/src/consts/template.js b/src/consts/template.js new file mode 100644 index 00000000..b806bbc8 --- /dev/null +++ b/src/consts/template.js @@ -0,0 +1,7 @@ +const allTemplateTypes = { + Normal: 1 +}; + +export default { + allTemplateTypes: allTemplateTypes, +} diff --git a/src/lib/category.js b/src/lib/category.js index 099286b0..d1f496c4 100644 --- a/src/lib/category.js +++ b/src/lib/category.js @@ -37,6 +37,10 @@ export function categoryTypeToTransactionType(categoryType) { } export function getTransactionPrimaryCategoryName(categoryId, allCategories) { + if (!allCategories) { + return ''; + } + for (let i = 0; i < allCategories.length; i++) { for (let j = 0; j < allCategories[i].subCategories.length; j++) { const subCategory = allCategories[i].subCategories[j]; @@ -50,6 +54,10 @@ export function getTransactionPrimaryCategoryName(categoryId, allCategories) { } export function getTransactionSecondaryCategoryName(categoryId, allCategories) { + if (!allCategories) { + return ''; + } + for (let i = 0; i < allCategories.length; i++) { for (let j = 0; j < allCategories[i].subCategories.length; j++) { const subCategory = allCategories[i].subCategories[j]; diff --git a/src/lib/services.js b/src/lib/services.js index 09256976..4e26dea2 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -517,6 +517,54 @@ export default { id }); }, + getAllTransactionTemplates: ({ templateType }) => { + return axios.get('v1/transaction/templates/list.json?templateType=' + templateType); + }, + getTransactionTemplate: ({ id }) => { + return axios.get('v1/transaction/templates/get.json?id=' + id); + }, + addTransactionTemplate: ({ templateType, name, clientSessionId }) => { + return axios.post('v1/transaction/templates/add.json', { + templateType, + name, + clientSessionId + }); + }, + modifyTransactionNameTemplate: ({ id, name }) => { + return axios.post('v1/transaction/templates/modify_name.json', { + id, + name + }); + }, + modifyTransactionTemplate: ({ id, type, categoryId, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, tagIds, comment }) => { + return axios.post('v1/transaction/templates/modify.json', { + id, + type, + categoryId, + sourceAccountId, + destinationAccountId, + sourceAmount, + destinationAmount, + tagIds, + comment + }); + }, + hideTransactionTemplate: ({ id, hidden }) => { + return axios.post('v1/transaction/templates/hide.json', { + id, + hidden + }); + }, + moveTransactionTemplate: ({ newDisplayOrders }) => { + return axios.post('v1/transaction/templates/move.json', { + newDisplayOrders, + }); + }, + deleteTransactionTemplate: ({ id }) => { + return axios.post('v1/transaction/templates/delete.json', { + id + }); + }, getLatestExchangeRates: ({ ignoreError }) => { return axios.get('v1/exchange_rates/latest.json', { ignoreError: !!ignoreError diff --git a/src/locales/en.js b/src/locales/en.js index 20c02c6c..6c2740c2 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -788,6 +788,7 @@ export default { 'Show': 'Show', 'Hide': 'Hide', 'Version': 'Version', + 'Modify Name': 'Modify Name', 'Edit': 'Edit', 'Remove': 'Remove', 'Delete': 'Delete', @@ -1020,6 +1021,7 @@ export default { 'Transactions': 'Transactions', 'Add Transaction': 'Add Transaction', 'Edit Transaction': 'Edit Transaction', + 'Edit Transaction Template': 'Edit Transaction Template', 'Modify Balance': 'Modify Balance', 'Expense Amount': 'Expense Amount', 'Income Amount': 'Income Amount', @@ -1268,6 +1270,23 @@ export default { 'Unable to delete this tag': 'Unable to delete this tag', 'Show Hidden Transaction Tags': 'Show Hidden Transaction Tags', 'Hide Hidden Transaction Tags': 'Hide Hidden Transaction Tags', + 'Transaction Templates': 'Transaction Templates', + 'Template Name': 'Template Name', + 'No available template': 'No available template', + 'Unable to retrieve template list': 'Unable to retrieve template list', + 'Template list is up to date': 'Template list is up to date', + 'Template list has been updated': 'Template list has been updated', + 'Unable to add template': 'Unable to add template', + 'Unable to save template': 'Unable to save template', + 'Unable to move template': 'Unable to move template', + 'Unable to retrieve template': 'Unable to retrieve template', + 'Unable to hide this template': 'Unable to hide this template', + 'Unable to unhide this template': 'Unable to unhide this template', + 'Are you sure you want to delete this template?': 'Are you sure you want to delete this template?', + 'Unable to delete this template': 'Unable to delete this template', + 'You have saved this template': 'You have saved this template', + 'Show Hidden Templates': 'Show Hidden Transaction Templates', + 'Hide Hidden Templates': 'Hide Hidden Transaction Templates', 'Are you sure you want to logout from this session?': 'Are you sure you want to logout from this session?', 'Unable to logout from this session': 'Unable to logout from this session', 'Are you sure you want to logout all other sessions?': 'Are you sure you want to logout all other sessions?', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 4da25a3e..a31f076a 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -788,6 +788,7 @@ export default { 'Show': '显示', 'Hide': '隐藏', 'Version': '版本', + 'Modify Name': '修改名称', 'Edit': '编辑', 'Remove': '移除', 'Delete': '删除', @@ -1020,6 +1021,7 @@ export default { 'Transactions': '交易', 'Add Transaction': '添加交易', 'Edit Transaction': '编辑交易', + 'Edit Transaction Template': '编辑交易模板', 'Modify Balance': '修改余额', 'Expense Amount': '支出金额', 'Income Amount': '收入金额', @@ -1268,6 +1270,23 @@ export default { 'Unable to delete this tag': '无法删除该标签', 'Show Hidden Transaction Tags': '显示隐藏的交易标签', 'Hide Hidden Transaction Tags': '不显示隐藏的交易标签', + 'Transaction Templates': '交易模板', + 'Template Name': '模板名称', + 'No available template': '没有可用的模板', + 'Unable to retrieve template list': '无法获取模板列表', + 'Template list is up to date': '模板列表已是最新', + 'Template list has been updated': '模板列表已更新', + 'Unable to add template': '无法添加模板', + 'Unable to save template': '无法保存模板', + 'Unable to move template': '无法移动模板', + 'Unable to retrieve template': '无法获取模板', + 'Unable to hide this template': '无法隐藏该模板', + 'Unable to unhide this template': '无法取消隐藏该模板', + 'Are you sure you want to delete this template?': '您确定要删除该模板?', + 'Unable to delete this template': '无法删除该模板', + 'You have saved this template': '您已经保存该模板', + 'Show Hidden Templates': '显示隐藏的模板', + 'Hide Hidden Templates': '不显示隐藏的模板', 'Are you sure you want to logout from this session?': '您确定要退出该会话?', 'Unable to logout from this session': '无法退出该会话', 'Are you sure you want to logout all other sessions?': '您确定要退出其他所有会话?', diff --git a/src/router/desktop.js b/src/router/desktop.js index 6c571bd1..38c4536c 100644 --- a/src/router/desktop.js +++ b/src/router/desktop.js @@ -22,6 +22,8 @@ import TransactionCategoryListPage from '@/views/desktop/categories/ListPage.vue import TransactionTagListPage from '@/views/desktop/tags/ListPage.vue'; +import TransactionTemplateListPage from '@/views/desktop/templates/ListPage.vue'; + import UserSettingsPage from '@/views/desktop/user/UserSettingsPage.vue'; import AppSettingsPage from '@/views/desktop/app/AppSettingsPage.vue'; @@ -136,6 +138,11 @@ const router = createRouter({ component: TransactionTagListPage, beforeEnter: checkLogin }, + { + path: '/template/list', + component: TransactionTemplateListPage, + beforeEnter: checkLogin + }, { path: '/exchange_rates', component: ExchangeRatesPage, diff --git a/src/router/mobile.js b/src/router/mobile.js index 41687770..2e54ed66 100644 --- a/src/router/mobile.js +++ b/src/router/mobile.js @@ -38,6 +38,8 @@ import CategoryPresetPage from '@/views/mobile/categories/PresetPage.vue'; import TagListPage from '@/views/mobile/tags/ListPage.vue'; +import TemplateListPage from '@/views/mobile/templates/ListPage.vue'; + function asyncResolve(component) { return function({ resolve }) { return resolve({ @@ -290,6 +292,11 @@ const routes = [ async: asyncResolve(TagListPage), beforeEnter: [checkLogin] }, + { + path: '/template/list', + async: asyncResolve(TemplateListPage), + beforeEnter: [checkLogin] + }, { path: '(.*)', redirect: '/' diff --git a/src/stores/index.js b/src/stores/index.js index e59fd971..ab9c3ebe 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -5,6 +5,7 @@ import { useUserStore } from './user.js'; import { useAccountsStore } from './account.js'; import { useTransactionCategoriesStore } from './transactionCategory.js'; import { useTransactionTagsStore } from './transactionTag.js'; +import { useTransactionTemplatesStore } from './transactionTemplate.js'; import { useTransactionsStore } from './transaction.js'; import { useOverviewStore } from './overview.js'; import { useStatisticsStore } from './statistics.js'; @@ -38,6 +39,9 @@ export const useRootStore = defineStore('root', { const transactionCategoriesStore = useTransactionCategoriesStore(); transactionCategoriesStore.resetTransactionCategories(); + const transactionTemplatesStore = useTransactionTemplatesStore(); + transactionTemplatesStore.resetTransactionTemplates(); + const accountsStore = useAccountsStore(); accountsStore.resetAccounts(); diff --git a/src/stores/transactionTemplate.js b/src/stores/transactionTemplate.js new file mode 100644 index 00000000..0c72a8bc --- /dev/null +++ b/src/stores/transactionTemplate.js @@ -0,0 +1,477 @@ +import { defineStore } from 'pinia'; + +import transactionConstants from '@/consts/transaction.js'; +import { isDefined, isObject, isArray, isEquals } from '@/lib/common.js'; +import services from '@/lib/services.js'; +import logger from '@/lib/logger.js'; + +function loadTransactionTemplateList(state, templateType, templates) { + state.allTransactionTemplates[templateType] = templates; + state.allTransactionTemplatesMap[templateType] = {}; + + for (let i = 0; i < templates.length; i++) { + const template = templates[i]; + state.allTransactionTemplatesMap[templateType][template.id] = template; + } +} + +function addTemplateToTransactionTemplateList(state, templateType, template) { + if (isArray(state.allTransactionTemplates[templateType])) { + state.allTransactionTemplates[templateType].push(template); + } + + if (isObject(state.allTransactionTemplatesMap[templateType])) { + state.allTransactionTemplatesMap[templateType][template.id] = template; + } +} + +function updateTemplateInTransactionTemplateList(state, templateType, template) { + if (isArray(state.allTransactionTemplates[templateType])) { + for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) { + if (state.allTransactionTemplates[templateType][i].id === template.id) { + state.allTransactionTemplates[templateType].splice(i, 1, template); + break; + } + } + } + + if (isObject(state.allTransactionTemplatesMap[templateType])) { + state.allTransactionTemplatesMap[templateType][template.id] = template; + } +} + +function updateTemplateDisplayOrderInTransactionTemplateList(state, templateType, { from, to }) { + if (isArray(state.allTransactionTemplates[templateType])) { + state.allTransactionTemplates[templateType].splice(to, 0, state.allTransactionTemplates[templateType].splice(from, 1)[0]); + } +} + +function updateTemplateVisibilityInTransactionTemplateList(state, templateType, { template, hidden }) { + if (isObject(state.allTransactionTemplatesMap[templateType])) { + if (state.allTransactionTemplatesMap[templateType][template.id]) { + state.allTransactionTemplatesMap[templateType][template.id].hidden = hidden; + } + } +} + +function removeTemplateFromTransactionTemplateList(state, templateType, template) { + if (isArray(state.allTransactionTemplates[templateType])) { + for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) { + if (state.allTransactionTemplates[templateType][i].id === template.id) { + state.allTransactionTemplates[templateType].splice(i, 1); + break; + } + } + } + + if (isObject(state.allTransactionTemplatesMap[templateType])) { + if (state.allTransactionTemplatesMap[templateType][template.id]) { + delete state.allTransactionTemplatesMap[templateType][template.id]; + } + } +} + +export const useTransactionTemplatesStore = defineStore('transactionTemplates', { + state: () => ({ + allTransactionTemplates: {}, + allTransactionTemplatesMap: {}, + transactionTemplateListStatesInvalid: {}, + }), + getters: { + allVisibleTemplates(state) { + const allVisibleTemplates = {}; + + for (const templateType in state.allTransactionTemplates) { + if (!Object.prototype.hasOwnProperty.call(state.allTransactionTemplates, templateType)) { + continue; + } + + const visibleTemplates = []; + + for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) { + const template = state.allTransactionTemplates[templateType][i]; + + if (!template.hidden) { + visibleTemplates.push(template); + } + } + + allVisibleTemplates[templateType] = visibleTemplates; + } + + return allVisibleTemplates; + }, + allAvailableTemplatesCount(state) { + const allAvailableTemplateCounts = {}; + + for (const templateType in state.allTransactionTemplates) { + if (!Object.prototype.hasOwnProperty.call(state.allTransactionTemplates, templateType)) { + continue; + } + + allAvailableTemplateCounts[templateType] = state.allTransactionTemplates[templateType].length; + } + + return allAvailableTemplateCounts; + }, + allVisibleTemplatesCount(state) { + const allVisibleTemplateCounts = {}; + + for (const templateType in state.allVisibleTemplates) { + if (!Object.prototype.hasOwnProperty.call(state.allVisibleTemplates, templateType)) { + continue; + } + + allVisibleTemplateCounts[templateType] = state.allVisibleTemplates[templateType].length; + } + + return allVisibleTemplateCounts; + } + }, + actions: { + generateNewTransactionTemplateModel(templateType) { + return { + id: '', + templateType: templateType, + name: '' + }; + }, + updateTransactionTemplateListInvalidState(templateType, invalidState) { + this.transactionTemplateListStatesInvalid[templateType] = invalidState; + }, + resetTransactionTemplates() { + this.allTransactionTemplates = {}; + this.allTransactionTemplatesMap = {}; + this.transactionTemplateListStatesInvalid = {}; + }, + loadAllTemplates({ templateType, force }) { + const self = this; + + if (!force && isDefined(self.transactionTemplateListStatesInvalid[templateType]) && !self.transactionTemplateListStatesInvalid[templateType]) { + return new Promise((resolve) => { + resolve(self.allTransactionTemplates[templateType] || []); + }); + } + + return new Promise((resolve, reject) => { + services.getAllTransactionTemplates({ templateType }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve template list' }); + return; + } + + if (!isDefined(self.transactionTemplateListStatesInvalid[templateType]) || self.transactionTemplateListStatesInvalid[templateType]) { + self.updateTransactionTemplateListInvalidState(templateType, false); + } + + if (force && data.result && isEquals(self.allTransactionTemplates[templateType], data.result)) { + reject({ message: 'Template list is up to date' }); + return; + } + + loadTransactionTemplateList(self, templateType, data.result); + + resolve(data.result); + }).catch(error => { + if (force) { + logger.error('failed to force load template list', error); + } else { + logger.error('failed to load template list', error); + } + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve template list' }); + } else { + reject(error); + } + }); + }); + }, + getTemplate({ templateId }) { + return new Promise((resolve, reject) => { + services.getTransactionTemplate({ + id: templateId + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve template' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to load template info', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve template' }); + } else { + reject(error); + } + }); + }); + }, + saveTemplateName({ template }) { + const self = this; + + return new Promise((resolve, reject) => { + let promise = null; + + if (!template.id) { + promise = services.addTransactionTemplate(template); + } else { + promise = services.modifyTransactionNameTemplate(template); + } + + promise.then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (!template.id) { + reject({ message: 'Unable to add template' }); + } else { + reject({ message: 'Unable to save template' }); + } + return; + } + + if (!template.id) { + addTemplateToTransactionTemplateList(self, template.templateType, data.result); + } else { + updateTemplateInTransactionTemplateList(self, template.templateType, data.result); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to save template', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (!template.id) { + reject({ message: 'Unable to add template' }); + } else { + reject({ message: 'Unable to save template' }); + } + } else { + reject(error); + } + }); + }); + }, + modifyTemplateContent({ template }) { + const self = this; + + const submitTemplate = { + id: template.id, + type: template.type, + sourceAccountId: template.sourceAccountId, + sourceAmount: template.sourceAmount, + destinationAccountId: '0', + destinationAmount: 0, + tagIds: template.tagIds, + comment: template.comment + }; + + if (template.type === transactionConstants.allTransactionTypes.Expense) { + submitTemplate.categoryId = template.expenseCategory; + } else if (template.type === transactionConstants.allTransactionTypes.Income) { + submitTemplate.categoryId = template.incomeCategory; + } else if (template.type === transactionConstants.allTransactionTypes.Transfer) { + submitTemplate.categoryId = template.transferCategory; + submitTemplate.destinationAccountId = template.destinationAccountId; + submitTemplate.destinationAmount = template.destinationAmount; + } else { + return Promise.reject('An error occurred'); + } + + return new Promise((resolve, reject) => { + services.modifyTransactionTemplate(submitTemplate).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to save template' }); + return; + } + + updateTemplateInTransactionTemplateList(self, template.templateType, data.result); + + resolve(data.result); + }).catch(error => { + logger.error('failed to save template', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to save template' }); + } else { + reject(error); + } + }); + }); + }, + changeTemplateDisplayOrder({ templateType, templateId, from, to }) { + const self = this; + + return new Promise((resolve, reject) => { + let template = null; + + if (!isArray(self.allTransactionTemplates[templateType])) { + reject({ message: 'Unable to move template' }); + return; + } + + for (let i = 0; i < self.allTransactionTemplates[templateType].length; i++) { + if (self.allTransactionTemplates[templateType][i].id === templateId) { + template = self.allTransactionTemplates[templateType][i]; + break; + } + } + + if (!template || !self.allTransactionTemplates[templateType][to]) { + reject({ message: 'Unable to move template' }); + return; + } + + if (isDefined(self.transactionTemplateListStatesInvalid[templateType]) && !self.transactionTemplateListStatesInvalid[templateType]) { + self.updateTransactionTemplateListInvalidState(templateType, true); + } + + updateTemplateDisplayOrderInTransactionTemplateList(self, templateType, { + template: template, + from: from, + to: to + }); + + resolve(); + }); + }, + updateTemplateDisplayOrders({ templateType }) { + const self = this; + const newDisplayOrders = []; + + if (isArray(self.allTransactionTemplates[templateType])) { + for (let i = 0; i < self.allTransactionTemplates[templateType].length; i++) { + newDisplayOrders.push({ + id: self.allTransactionTemplates[templateType][i].id, + displayOrder: i + 1 + }); + } + } + + return new Promise((resolve, reject) => { + services.moveTransactionTemplate({ + newDisplayOrders: newDisplayOrders + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to move template' }); + return; + } + + if (!isDefined(self.transactionTemplateListStatesInvalid[templateType]) || self.transactionTemplateListStatesInvalid[templateType]) { + self.updateTransactionTemplateListInvalidState(templateType, false); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to save templates display order', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to move template' }); + } else { + reject(error); + } + }); + }); + }, + hideTemplate({ template, hidden }) { + const self = this; + + return new Promise((resolve, reject) => { + services.hideTransactionTemplate({ + id: template.id, + hidden: hidden + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (hidden) { + reject({ message: 'Unable to hide this template' }); + } else { + reject({ message: 'Unable to unhide this template' }); + } + + return; + } + + updateTemplateVisibilityInTransactionTemplateList(self, template.templateType, { + template: template, + hidden: hidden + }); + + resolve(data.result); + }).catch(error => { + logger.error('failed to change template visibility', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (hidden) { + reject({ message: 'Unable to hide this template' }); + } else { + reject({ message: 'Unable to unhide this template' }); + } + } else { + reject(error); + } + }); + }); + }, + deleteTemplate({ template, beforeResolve }) { + const self = this; + + return new Promise((resolve, reject) => { + services.deleteTransactionTemplate({ + id: template.id + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to delete this template' }); + return; + } + + if (beforeResolve) { + beforeResolve(() => { + removeTemplateFromTransactionTemplateList(self, template.templateType, template); + }); + } else { + removeTemplateFromTransactionTemplateList(self, template.templateType, template); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to delete template', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to delete this template' }); + } else { + reject(error); + } + }); + }); + } + } +}); diff --git a/src/views/desktop/MainLayout.vue b/src/views/desktop/MainLayout.vue index dee7f619..c04b47f1 100644 --- a/src/views/desktop/MainLayout.vue +++ b/src/views/desktop/MainLayout.vue @@ -61,6 +61,12 @@ {{ $t('Transaction Tags') }} +